diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index c9a25670a907..000000000000 --- a/.eslintignore +++ /dev/null @@ -1,29 +0,0 @@ -**/build -**/dist -**/coverage -.angular -storybook-static - -**/node_modules - -**/webpack.*.js -**/jest.config.js - -apps/browser/config/config.js -apps/browser/src/auth/scripts/duo.js -apps/browser/webpack/manifest.js - -apps/desktop/desktop_native -apps/desktop/src/auth/scripts/duo.js - -apps/web/config.js -apps/web/scripts/*.js -apps/web/tailwind.config.js - -apps/cli/config/config.js - -tailwind.config.js -libs/components/tailwind.config.base.js -libs/components/tailwind.config.js - -scripts/*.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3fd6dec3d7ec..000000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,258 +0,0 @@ -{ - "root": true, - "env": { - "browser": true, - "webextensions": true - }, - "overrides": [ - { - "files": ["*.ts", "*.js"], - "plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": ["./tsconfig.eslint.json"], - "sourceType": "module", - "ecmaVersion": 2020 - }, - "extends": [ - "eslint:recommended", - "plugin:@angular-eslint/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - "plugin:rxjs/recommended", - "prettier", - "plugin:storybook/recommended" - ], - "settings": { - "import/parsers": { - "@typescript-eslint/parser": [".ts"] - }, - "import/resolver": { - "typescript": { - "alwaysTryTypes": true - } - } - }, - "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" } - ], - "@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled - "@typescript-eslint/no-floating-promises": "error", - "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }], - "@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }], - "@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }], - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "no-console": "error", - "import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package. - "import/order": [ - "error", - { - "alphabetize": { - "order": "asc" - }, - "newlines-between": "always", - "pathGroups": [ - { - "pattern": "@bitwarden/**", - "group": "external", - "position": "after" - }, - { - "pattern": "src/**/*", - "group": "parent", - "position": "before" - } - ], - "pathGroupsExcludedImportTypes": ["builtin"] - } - ], - "rxjs-angular/prefer-takeuntil": ["error", { "alias": ["takeUntilDestroyed"] }], - "rxjs/no-exposed-subjects": ["error", { "allowProtected": true }], - "no-restricted-syntax": [ - "error", - { - "message": "Calling `svgIcon` directly is not allowed", - "selector": "CallExpression[callee.name='svgIcon']" - }, - { - "message": "Accessing FormGroup using `get` is not allowed, use `.value` instead", - "selector": "ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']" - } - ], - "curly": ["error", "all"], - "import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway - "import/no-restricted-paths": [ - "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/**/*", - "from": [ - // Angular - "./libs/angular/**/*", - "./node_modules/@angular*/**/*", - - // Node - "./libs/node/**/*", - - //Generator - "./libs/tools/generator/components/**/*", - "./libs/tools/generator/core/**/*", - "./libs/tools/generator/extensions/**/*", - - // Import/export - "./libs/importer/**/*", - "./libs/tools/export/vault-export/vault-export-core/**/*" - ] - }, - { - // avoid import of unexported state objects - "target": [ - "!(libs)/**/*", - "libs/!(common)/**/*", - "libs/common/!(src)/**/*", - "libs/common/src/!(platform)/**/*", - "libs/common/src/platform/!(state)/**/*" - ], - "from": ["./libs/common/src/platform/state/**/*"], - // allow module index import - "except": ["**/state/index.ts"] - } - ] - } - ] - } - }, - { - "files": ["*.html"], - "parser": "@angular-eslint/template-parser", - "plugins": ["@angular-eslint/template", "tailwindcss"], - "rules": { - "@angular-eslint/template/button-has-type": "error", - "tailwindcss/no-custom-classname": [ - "error", - { - // uses negative lookahead to whitelist any class that doesn't start with "tw-" - // in other words: classnames that start with tw- must be valid TailwindCSS classes - "whitelist": ["(?!(tw)\\-).*"] - } - ], - "tailwindcss/enforces-negative-arbitrary-values": "error", - "tailwindcss/enforces-shorthand": "error", - "tailwindcss/no-contradicting-classname": "error" - } - }, - { - "files": ["apps/browser/src/**/*.ts", "libs/**/*.ts"], - "excludedFiles": [ - "apps/browser/src/autofill/{content,notification}/**/*.ts", - "apps/browser/src/**/background/**/*.ts", // It's okay to have long lived listeners in the background - "apps/browser/src/platform/background.ts" - ], - "rules": { - "no-restricted-syntax": [ - "error", - { - "message": "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead", - // This selector covers events like chrome.storage.onChange & chrome.runtime.onMessage - "selector": "CallExpression > [object.object.object.name='chrome'][property.name='addListener']" - }, - { - "message": "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead", - // This selector covers events like chrome.storage.local.onChange - "selector": "CallExpression > [object.object.object.object.name='chrome'][property.name='addListener']" - } - ] - } - }, - { - "files": ["**/*.ts"], - "excludedFiles": ["**/platform/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { - "patterns": [ - "**/platform/**/internal", // General internal pattern - // All features that have been converted to barrel files - "**/platform/messaging/**" - ] - } - ] - } - }, - { - "files": ["**/src/**/*.ts"], - "excludedFiles": ["**/platform/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { - "patterns": [ - "**/platform/**/internal", // General internal pattern - // All features that have been converted to barrel files - "**/platform/messaging/**", - "**/src/**/*" // Prevent relative imports across libs. - ] - } - ] - } - }, - { - "files": ["bitwarden_license/bit-common/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/bit-common/*", "**/src/**/*"] } - ] - } - }, - { - "files": ["apps/**/*.ts"], - "rules": { - // Catches static imports - "no-restricted-imports": [ - "error", - { - "patterns": [ - "biwarden_license/**", - "@bitwarden/bit-common/*", - "@bitwarden/bit-web/*", - "**/src/**/*" - ] - } - ], - // Catches dynamic imports, e.g. in routing modules where modules are lazy-loaded - "no-restricted-syntax": [ - "error", - { - "message": "Don't import Bitwarden licensed code into OSS code.", - "selector": "ImportExpression > Literal.source[value=/.*(bitwarden_license|bit-common|bit-web).*/]" - } - ] - } - } - ] -} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cb36d87b9e13..7b6d24aa8c0d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,12 +29,12 @@ libs/tools @bitwarden/team-tools-dev bitwarden_license/bit-web/src/app/tools @bitwarden/team-tools-dev bitwarden_license/bit-common/src/tools @bitwarden/team-tools-dev -## Localization/Crowdin (Tools team) -apps/browser/src/_locales @bitwarden/team-tools-dev -apps/browser/store/locales @bitwarden/team-tools-dev -apps/cli/src/locales @bitwarden/team-tools-dev -apps/desktop/src/locales @bitwarden/team-tools-dev -apps/web/src/locales @bitwarden/team-tools-dev +## Localization/Crowdin (Platform and Tools team) +apps/browser/src/_locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev +apps/browser/store/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev +apps/cli/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev +apps/desktop/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev +apps/web/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev ## Vault team files ## apps/browser/src/vault @bitwarden/team-vault-dev @@ -58,6 +58,7 @@ libs/admin-console @bitwarden/team-admin-console-dev ## Billing team files ## apps/browser/src/billing @bitwarden/team-billing-dev +apps/desktop/src/billing @bitwarden/team-billing-dev apps/web/src/app/billing @bitwarden/team-billing-dev libs/angular/src/billing @bitwarden/team-billing-dev libs/common/src/billing @bitwarden/team-billing-dev @@ -85,9 +86,13 @@ apps/web/src/app/shared @bitwarden/team-platform-dev apps/web/src/translation-constants.ts @bitwarden/team-platform-dev # Workflows .github/workflows/brew-bump-desktop.yml @bitwarden/team-platform-dev +.github/workflows/build-browser-target.yml @bitwarden/team-platform-dev .github/workflows/build-browser.yml @bitwarden/team-platform-dev +.github/workflows/build-cli-target.yml @bitwarden/team-platform-dev .github/workflows/build-cli.yml @bitwarden/team-platform-dev +.github/workflows/build-desktop-target.yml @bitwarden/team-platform-dev .github/workflows/build-desktop.yml @bitwarden/team-platform-dev +.github/workflows/build-web-target.yml @bitwarden/team-platform-dev .github/workflows/build-web.yml @bitwarden/team-platform-dev .github/workflows/chromatic.yml @bitwarden/team-platform-dev .github/workflows/lint.yml @bitwarden/team-platform-dev @@ -96,24 +101,28 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev .github/workflows/scan.yml @bitwarden/team-platform-dev .github/workflows/test.yml @bitwarden/team-platform-dev .github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev +# ESLint custom rules +libs/eslint @bitwarden/team-platform-dev ## Autofill team files ## apps/browser/src/autofill @bitwarden/team-autofill-dev apps/desktop/src/autofill @bitwarden/team-autofill-dev libs/common/src/autofill @bitwarden/team-autofill-dev apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev +apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev # SSH Agent apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-dev @bitwarden/wg-ssh-keys -## Component Library ## -.storybook @bitwarden/team-design-system -libs/components @bitwarden/team-design-system -apps/browser/src/platform/popup/layout @bitwarden/team-design-system -apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-design-system -apps/web/src/app/layouts @bitwarden/team-design-system +## UI Foundation ## +.storybook @bitwarden/team-ui-foundation +libs/components @bitwarden/team-ui-foundation +libs/ui @bitwarden/team-ui-foundation +apps/browser/src/platform/popup/layout @bitwarden/team-ui-foundation +apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-ui-foundation +apps/web/src/app/layouts @bitwarden/team-ui-foundation ## Desktop native module ## apps/desktop/desktop_native @bitwarden/team-platform-dev @@ -126,9 +135,10 @@ apps/web/src/app/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/key-management @bitwarden/team-key-management-dev +libs/key-management-ui @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev -apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev +apps/desktop/desktop_native/core/src/biometric/ @bitwarden/team-key-management-dev apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev apps/browser/src/background/nativeMessaging.background.ts @bitwarden/team-key-management-dev apps/desktop/src/services/biometric-message-handler.service.ts @bitwarden/team-key-management-dev @@ -161,3 +171,7 @@ apps/web/src/locales/en/messages.json **/*.Dockerfile **/.dockerignore **/entrypoint.sh + +## Overrides +# tsconfig files are potentially dangerous and will be reviewed by platform to prevent misconfigurations +**/tsconfig.json @bitwarden/team-platform-dev diff --git a/.github/codecov.yml b/.github/codecov.yml index b47744072066..0a6b3ceacff3 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,2 +1,66 @@ ignore: - "**/*.spec.ts" # Tests + +component_management: + default_rules: + statuses: + - type: project + target: auto + individual_components: + - component_id: key-management-biometrics + name: Key Management - Biometrics + paths: + - apps/browser/src/key-management/biometrics/** + - apps/cli/src/key-management/cli-biometrics-service.ts + - apps/desktop/destkop_native/core/src/biometric/** + - apps/desktop/src/key-management/biometrics/** + - apps/desktop/src/services/biometric-message-handler.service.ts + - apps/web/src/app/key-management/web-biometric.service.ts + - libs/key-management/src/biometrics/** + - component_id: key-management-lock + name: Key Management - Lock + paths: + - apps/browser/src/key-management/lock/** + - apps/desktop/src/key-management/lock/** + - apps/web/src/app/key-management/lock/** + - libs/key-management-ui/src/lock/** + - component_id: key-management-ipc + name: Key Management - IPC + paths: + - apps/browser/src/background/nativeMessaging.background.ts + - apps/desktop/src/services/native-messaging.service.ts + - component_id: key-management-key-rotation + name: Key Management - Key Rotation + paths: + - apps/web/src/app/key-management/key-rotation/** + - apps/web/src/app/key-management/migrate-encryption/** + - libs/key-management/src/user-asymmetric-key-regeneration/** + - component_id: key-management-process-reload + name: Key Management - Process Reload + paths: + - apps/web/src/app/key-management/services/web-process-reload.service.ts + - libs/common/src/key-management/services/default-process-reload.service.ts + - component_id: key-management-keys + name: Key Management - Keys + paths: + - libs/key-management/src/kdf-config.service.ts + - libs/key-management/src/key.service.ts + - libs/common/src/key-management/master-password/** + - component_id: key-management-crypto + name: Key Management - Crypto + paths: + - libs/common/src/key-management/crypto/** + - component_id: key-management + name: Key Management + paths: + - apps/browser/src/key-management/** + - apps/browser/src/background/nativeMessaging.background.ts + - apps/cli/src/key-management/** + - apps/desktop/desktop_native/core/src/biometric/** + - apps/desktop/src/key-management/** + - apps/desktop/src/services/biometric-message-handler.service.ts + - apps/desktop/src/services/native-messaging.service.ts + - apps/web/src/app/key-management/** + - libs/common/src/key-management/** + - libs/key-management/** + - libs/key-management-ui/** diff --git a/.github/renovate.json b/.github/renovate.json index b5c43cc1d39f..f1efcbaffbea 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -48,7 +48,6 @@ "css-loader", "html-loader", "mini-css-extract-plugin", - "ngx-infinite-scroll", "postcss", "postcss-loader", "process", @@ -69,6 +68,26 @@ "commitMessagePrefix": "[deps] Auth:", "reviewers": ["team:team-auth-dev"] }, + { + "matchPackageNames": [ + "@angular-eslint/schematics", + "angular-eslint", + "eslint-config-prettier", + "eslint-import-resolver-typescript", + "eslint-plugin-import", + "eslint-plugin-rxjs-angular", + "eslint-plugin-rxjs", + "eslint-plugin-storybook", + "eslint-plugin-tailwindcss", + "eslint", + "husky", + "lint-staged", + "typescript-eslint" + ], + "description": "Architecture owned dependencies", + "commitMessagePrefix": "[deps] Architecture:", + "reviewers": ["team:dept-architecture"] + }, { "matchPackageNames": [ "@angular-eslint/eslint-plugin-template", @@ -88,9 +107,8 @@ "husky", "lint-staged" ], - "description": "Architecture owned dependencies", - "commitMessagePrefix": "[deps] Architecture:", - "reviewers": ["team:dept-architecture"] + "groupName": "Linting minor-patch", + "matchUpdateTypes": ["minor", "patch"] }, { "matchPackageNames": [ @@ -193,6 +211,8 @@ "@storybook/angular", "@storybook/manager-api", "@storybook/theming", + "@typescript-eslint/utils", + "@typescript-eslint/rule-tester", "@types/react", "autoprefixer", "bootstrap", @@ -207,9 +227,9 @@ "tailwindcss", "zone.js" ], - "description": "Component library owned dependencies", - "commitMessagePrefix": "[deps] Design System:", - "reviewers": ["team:team-design-system"] + "description": "UI Foundation owned dependencies", + "commitMessagePrefix": "[deps] UI Foundation:", + "reviewers": ["team:team-ui-foundation"] }, { "matchPackageNames": [ diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index 73d323851e50..653f6591c7f4 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -3,26 +3,12 @@ ./apps/browser/src/safari/desktop/Assets.xcassets/AppIcon.appiconset ./apps/browser/src/safari/desktop/Base.lproj ./apps/browser/store/windows/Assets -./bitwarden_license/README.md ./libs/angular/src/directives/cipherListVirtualScroll.directive.ts -./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/key-management/README.md -./libs/tools/README.md -./libs/tools/export/vault-export/README.md -./libs/tools/send/README.md -./libs/tools/card/README.md -./libs/vault/README.md -./README.md ./LICENSE_BITWARDEN.txt ./CONTRIBUTING.md ./LICENSE_GPL.txt ./LICENSE.txt ./apps/web/Dockerfile -./apps/web/README.md ./apps/desktop/resources/installerSidebar.bmp ./apps/desktop/resources/appx/SplashScreen.png ./apps/desktop/resources/appx/BadgeLogo.png @@ -30,10 +16,7 @@ ./apps/desktop/resources/appx/StoreLogo.png ./apps/desktop/resources/appx/Wide310x150Logo.png ./apps/desktop/resources/appx/Square44x44Logo.png -./apps/desktop/README.md ./apps/cli/stores/chocolatey/tools/VERIFICATION.txt -./apps/cli/README.md -./apps/browser/README.md ./apps/browser/store/windows/AppxManifest.xml ./apps/browser/src/background/nativeMessaging.background.ts ./apps/browser/src/models/browserComponentState.ts diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml new file mode 100644 index 000000000000..3334326920cb --- /dev/null +++ b/.github/workflows/build-browser-target.yml @@ -0,0 +1,33 @@ +name: Build Browser on PR Target + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: + - 'l10n_master' + - 'cf-pages' + paths: + - 'apps/browser/**' + - 'libs/**' + - '*' + - '!*.md' + - '!*.txt' + workflow_call: + inputs: {} + +defaults: + run: + shell: bash + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + run-workflow: + name: Run Build Browser on PR Target + needs: check-run + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ./.github/workflows/build-browser.yml + secrets: inherit + diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 64cbaa0c7f1a..d9cac5878043 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -1,7 +1,7 @@ name: Build Browser on: - pull_request_target: + pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -38,19 +38,14 @@ defaults: shell: bash jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - setup: name: Setup runs-on: ubuntu-22.04 - needs: - - check-run outputs: repo_url: ${{ steps.gen_vars.outputs.repo_url }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} + has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -74,6 +69,14 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + - name: Check secrets + id: check-secrets + env: + AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + run: | + has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + locales-test: name: Locales Test @@ -169,7 +172,7 @@ jobs: zip -r browser-source.zip browser-source - name: Upload browser source - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: browser-source-${{ env._BUILD_NUMBER }}.zip path: browser-source.zip @@ -208,7 +211,11 @@ jobs: - name: "opera" npm_command: "dist:opera" archive_name: "dist-opera.zip" - artifact_name: "dist-opera-MV3" + artifact_name: "dist-opera" + - name: "opera-mv3" + npm_command: "dist:opera:mv3" + archive_name: "dist-opera.zip" + artifact_name: "DO-NOT-USE-FOR-PROD-dist-opera-MV3" steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -264,7 +271,7 @@ jobs: working-directory: browser-source/apps/browser - name: Upload extension artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ matrix.artifact_name }}-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/${{ matrix.archive_name }} @@ -277,6 +284,7 @@ jobs: needs: - setup - locales-test + if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -401,7 +409,7 @@ jobs: ls -la - name: Upload Safari artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: dist-safari-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/dist-safari.zip diff --git a/.github/workflows/build-cli-target.yml b/.github/workflows/build-cli-target.yml new file mode 100644 index 000000000000..81ec41786810 --- /dev/null +++ b/.github/workflows/build-cli-target.yml @@ -0,0 +1,33 @@ +name: Build CLI on PR Target + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: + - 'l10n_master' + - 'cf-pages' + paths: + - 'apps/cli/**' + - 'libs/**' + - '*' + - '!*.md' + - '!*.txt' + - '.github/workflows/build-cli.yml' + - 'bitwarden_license/bit-cli/**' + +defaults: + run: + shell: bash + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + run-workflow: + name: Run Build CLI on PR Target + needs: check-run + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ./.github/workflows/build-cli.yml + secrets: inherit + diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 02432e0a5f48..b3694ac423bf 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -1,7 +1,7 @@ name: Build CLI on: - pull_request_target: + pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -27,6 +27,8 @@ on: - '!*.txt' - '.github/workflows/build-cli.yml' - 'bitwarden_license/bit-cli/**' + workflow_call: + inputs: {} workflow_dispatch: inputs: sdk_branch: @@ -39,18 +41,13 @@ defaults: working-directory: apps/cli jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - setup: name: Setup runs-on: ubuntu-22.04 - needs: - - check-run outputs: package_version: ${{ steps.retrieve-package-version.outputs.package_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} + has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -71,6 +68,14 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + - name: Check secrets + id: check-secrets + env: + AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + run: | + has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + cli: name: CLI ${{ matrix.os.base }} - ${{ matrix.license_type.readable }} strategy: @@ -117,7 +122,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -130,7 +135,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -163,14 +168,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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 @@ -272,7 +277,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -285,7 +290,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -324,14 +329,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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 @@ -339,7 +344,7 @@ jobs: - name: Upload Chocolatey asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg @@ -350,7 +355,7 @@ jobs: - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip @@ -421,14 +426,14 @@ jobs: run: sudo snap remove bw - name: Upload snap asset - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml new file mode 100644 index 000000000000..8c26f9911746 --- /dev/null +++ b/.github/workflows/build-desktop-target.yml @@ -0,0 +1,32 @@ +name: Build Desktop on PR Target + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: + - 'l10n_master' + - 'cf-pages' + paths: + - 'apps/desktop/**' + - 'libs/**' + - '*' + - '!*.md' + - '!*.txt' + - '.github/workflows/build-desktop.yml' + +defaults: + run: + shell: bash + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + run-workflow: + name: Run Build Desktop on PR Target + needs: check-run + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ./.github/workflows/build-desktop.yml + secrets: inherit + diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index b27d1486bd2c..23722e7c7dfe 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1,7 +1,7 @@ name: Build Desktop on: - pull_request_target: + pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -25,6 +25,8 @@ on: - '!*.md' - '!*.txt' - '.github/workflows/build-desktop.yml' + workflow_call: + inputs: {} workflow_dispatch: inputs: sdk_branch: @@ -37,15 +39,9 @@ defaults: shell: bash jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - electron-verify: name: Verify Electron Version runs-on: ubuntu-22.04 - needs: - - check-run steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -67,8 +63,6 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 - needs: - - check-run outputs: package_version: ${{ steps.retrieve-version.outputs.package_version }} release_channel: ${{ steps.release-channel.outputs.channel }} @@ -76,6 +70,7 @@ jobs: rc_branch_exists: ${{ steps.branch-check.outputs.rc_branch_exists }} hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} + has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} defaults: run: working-directory: apps/desktop @@ -138,6 +133,14 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + - name: Check secrets + id: check-secrets + env: + AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + run: | + has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + linux: name: Linux Build # Note, before updating the ubuntu version of the workflow, ensure the snap base image @@ -232,42 +235,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml @@ -280,7 +283,7 @@ jobs: sudo npm run pack:lin:flatpak - name: Upload flatpak artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: com.bitwarden.desktop.flatpak path: apps/desktop/dist/com.bitwarden.desktop.flatpak @@ -333,12 +336,14 @@ jobs: rustup show - name: Login to Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" @@ -353,7 +358,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -366,7 +371,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -386,7 +391,17 @@ jobs: working-directory: apps/desktop/desktop_native run: node build.js cross-platform - - name: Build & Sign (dev) + - name: Build + run: | + npm run build + + - name: Pack + if: ${{ needs.setup.outputs.has_secrets == 'false' }} + run: | + npm run pack:win + + - name: Pack & Sign (dev) + if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: ELECTRON_BUILDER_SIGN: 1 SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }} @@ -395,10 +410,10 @@ jobs: SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }} SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }} run: | - npm run build npm run pack:win - name: Rename appx files for store + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Copy-Item "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx" ` -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx" @@ -408,6 +423,7 @@ jobs: -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx" - name: Package for Chocolatey + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Copy-Item -Path ./stores/chocolatey -Destination ./dist/chocolatey -Recurse Copy-Item -Path ./dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe ` @@ -419,6 +435,7 @@ jobs: choco pack ./dist/chocolatey/bitwarden.nuspec --version "$env:_PACKAGE_VERSION" --out ./dist/chocolatey - name: Fix NSIS artifact names for auto-updater + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Rename-Item -Path .\dist\nsis-web\Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z ` -NewName bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -428,91 +445,103 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -574,11 +603,13 @@ jobs: key: ${{ runner.os }}-${{ github.run_id }}-safari-extension - name: Login to Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Download Provisioning Profiles secrets + if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: ACCOUNT_NAME: bitwardenci CONTAINER_NAME: profiles @@ -591,6 +622,7 @@ jobs: --output none - name: Get certificates + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | mkdir -p $HOME/certificates @@ -613,6 +645,7 @@ jobs: jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 - name: Set up keychain + if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | @@ -642,6 +675,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain - name: Set up provisioning profiles + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile @@ -661,7 +695,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -674,7 +708,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -701,6 +735,7 @@ jobs: browser-build: name: Browser Build needs: setup + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: ./.github/workflows/build-browser.yml secrets: inherit @@ -708,6 +743,7 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets runs-on: macos-13 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build @@ -918,28 +954,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -949,6 +985,7 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset runs-on: macos-13 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build @@ -1166,7 +1203,7 @@ jobs: run: npm run pack:mac:mas - name: Upload .pkg artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -1217,6 +1254,7 @@ jobs: macos-package-dev: name: MacOS Package Dev Release Asset runs-on: macos-13 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build @@ -1425,7 +1463,7 @@ jobs: zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - name: Upload masdev artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml new file mode 100644 index 000000000000..fb7074292b56 --- /dev/null +++ b/.github/workflows/build-web-target.yml @@ -0,0 +1,32 @@ +name: Build Web on PR Target + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: + - 'l10n_master' + - 'cf-pages' + paths: + - 'apps/web/**' + - 'libs/**' + - '*' + - '!*.md' + - '!*.txt' + - '.github/workflows/build-web.yml' + +defaults: + run: + shell: bash + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + run-workflow: + name: Run Build Web on PR Target + needs: check-run + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ./.github/workflows/build-web.yml + secrets: inherit + diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 73ae0e149628..423b15372aed 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -1,7 +1,7 @@ name: Build Web on: - pull_request_target: + pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -27,6 +27,8 @@ on: - '.github/workflows/build-web.yml' release: types: [published] + workflow_call: + inputs: {} workflow_dispatch: inputs: custom_tag_extension: @@ -41,18 +43,13 @@ env: _AZ_REGISTRY: bitwardenprod.azurecr.io jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - setup: name: Setup runs-on: ubuntu-22.04 - needs: - - check-run outputs: version: ${{ steps.version.outputs.value }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} + has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -70,6 +67,14 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + - name: Check secrets + id: check-secrets + env: + AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + run: | + has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + build-artifacts: name: Build artifacts runs-on: ubuntu-22.04 @@ -128,7 +133,7 @@ jobs: run: npm ci - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -141,7 +146,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -164,7 +169,7 @@ jobs: run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build - name: Upload ${{ matrix.name }} artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: web-${{ env._VERSION }}-${{ matrix.name }}.zip path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip @@ -213,19 +218,23 @@ jobs: ########## ACRs ########## - name: Login to Prod Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - name: Log into Prod container registry + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: az acr login -n bitwardenprod - name: Login to Azure - CI Subscription + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve github PAT secrets + if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: retrieve-secret-pat uses: bitwarden/gh-actions/get-keyvault-secrets@main with: @@ -273,8 +282,9 @@ jobs: run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT - name: Build Docker image + if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: build-docker - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 with: context: apps/web file: apps/web/Dockerfile @@ -283,7 +293,7 @@ jobs: tags: ${{ steps.image-name.outputs.name }} secrets: | "GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}" - + - name: Install Cosign if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 @@ -310,7 +320,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index a5ebd363f632..75a074319426 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -56,7 +56,7 @@ jobs: run: npm run build-storybook:ci - name: Publish to Chromatic - uses: chromaui/action@64a9c0ca3bfb724389b0d536e544f56b7b5ff5b3 # v11.20.2 + uses: chromaui/action@8a12962215a66cd05b1ac5b0f1c08768d1aab155 # v11.25.0 with: token: ${{ secrets.GITHUB_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 867de3844e78..4fbef027c7ca 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,12 +1,20 @@ name: Lint on: - push: + pull_request: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' paths-ignore: - '.github/workflows/**' + push: + branches: + - 'main' + - 'rc' + - 'hotfix-rc-*' + paths-ignore: + - '.github/workflows/**' workflow_dispatch: inputs: {} @@ -34,6 +42,7 @@ jobs: ! -path "*/.DS_Store" \ ! -path "*/*locales/*" \ ! -path "./.github/*" \ + ! -path "*/README.md" \ ! -path "*/Cargo.toml" \ ! -path "*/Cargo.lock" \ ! -path "./apps/desktop/macos/*" \ diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index ff85a30d3f60..d758e6f11c9a 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -222,7 +222,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -230,4 +230,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 69ccd8410652..ae631165db98 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -7,19 +7,18 @@ on: publish_type: description: 'Publish Options' required: true - default: 'Initial Publish' + default: 'Publish' type: choice options: - - Initial Publish - - Republish + - Publish - Dry Run version: description: 'Version to publish (default: latest desktop release)' required: true type: string default: latest - rollout_percentage: - description: 'Staged Rollout Percentage' + electron_rollout_percentage: + description: 'Staged Rollout Percentage for Electron' required: true default: '10' type: string @@ -137,7 +136,7 @@ jobs: - name: Set staged rollout percentage env: RELEASE_CHANNEL: ${{ needs.setup.outputs.release_channel }} - ROLLOUT_PCT: ${{ inputs.rollout_percentage }} + ROLLOUT_PCT: ${{ inputs.electron_rollout_percentage }} run: | echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml @@ -163,7 +162,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -171,7 +170,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} snap: name: Deploy Snap @@ -284,7 +283,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -292,4 +291,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 7e8722dc79f7..498f87489599 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ github.event.inputs.release_type }} project-type: ts @@ -128,7 +128,7 @@ jobs: - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 with: artifacts: 'browser-source-${{ needs.setup.outputs.release_version }}.zip, dist-chrome-${{ needs.setup.outputs.release_version }}.zip, diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index d16cd744d7de..6a10bec1ba2c 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts @@ -73,7 +73,7 @@ jobs: - name: Create release if: ${{ inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 env: PKG_VERSION: ${{ needs.setup.outputs.release_version }} with: diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 08174dc552e8..a5e374395d84 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -47,7 +47,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: 'Initial Release' project-type: ts @@ -158,42 +158,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml @@ -299,91 +299,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -707,28 +707,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -915,7 +915,7 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - name: Upload .pkg artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index ba934235b442..57143747a869 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts @@ -96,7 +96,7 @@ jobs: file_path: "apps/desktop/artifacts/sha256-checksums.txt" - name: Create Release - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 if: ${{ steps.release_channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} env: PKG_VERSION: ${{ steps.version.outputs.version }} diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index faa398f6d67c..0301b814796e 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -81,7 +81,7 @@ jobs: - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 with: name: "Web v${{ needs.setup.outputs.release_version }}" commit: ${{ github.sha }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index a09e8137b656..ac7f0ae6f718 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: cx_result.sarif diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 6caa7b993314..abb292f53f3a 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: 'Run stale action' - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: stale-issue-label: 'needs-reply' stale-pr-label: 'needs-changes' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72bc3594beb0..8c214b99ed3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,13 +85,10 @@ jobs: fail-on-error: true - name: Upload coverage to codecov.io - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 - if: ${{ needs.check-test-secrets.outputs.available == 'true' }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 - name: Upload results to codecov.io - uses: codecov/test-results-action@9739113ad922ea0a9abb4b2c0f8bf6a4aa8ef820 # v1.0.1 + uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2 if: ${{ needs.check-test-secrets.outputs.available == 'true' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -106,15 +103,15 @@ jobs: matrix: os: - ubuntu-22.04 - - macos-latest - - windows-latest + - macos-14 + - windows-2022 steps: - name: Check Rust version run: rustup --version - name: Install gnome-keyring - if: ${{ matrix.os=='ubuntu-latest' }} + if: ${{ matrix.os=='ubuntu-22.04' }} run: | sudo apt-get update sudo apt-get install -y gnome-keyring dbus-x11 @@ -127,7 +124,7 @@ jobs: run: cargo build - name: Test Ubuntu - if: ${{ matrix.os=='ubuntu-latest' }} + if: ${{ matrix.os=='ubuntu-22.04' }} working-directory: ./apps/desktop/desktop_native run: | eval "$(dbus-launch --sh-syntax)" @@ -138,11 +135,41 @@ jobs: cargo test -- --test-threads=1 - name: Test macOS - if: ${{ matrix.os=='macos-latest' }} + if: ${{ matrix.os=='macos-14' }} working-directory: ./apps/desktop/desktop_native run: cargo test -- --test-threads=1 - name: Test Windows - if: ${{ matrix.os=='windows-latest'}} + if: ${{ matrix.os=='windows-2022'}} working-directory: ./apps/desktop/desktop_native/core run: cargo test -- --test-threads=1 + + rust-coverage: + name: Rust Coverage + runs-on: macos-14 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install rust + uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203 # stable + with: + toolchain: stable + components: llvm-tools + + - name: Cache cargo registry + uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5 + with: + workspaces: "apps/desktop/desktop_native -> target" + + - name: Install cargo-llvm-cov + run: cargo install cargo-llvm-cov --version 0.6.16 + + - name: Generate coverage + working-directory: ./apps/desktop/desktop_native + run: cargo llvm-cov --all-features --lcov --output-path lcov.info --workspace --no-cfg-coverage + + - name: Upload to codecov.io + uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 + with: + files: ./apps/desktop/desktop_native/lcov.info diff --git a/.storybook/main.ts b/.storybook/main.ts index b48a86ba2b20..d98ca06ead39 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,8 @@ import { dirname, join } from "path"; + import { StorybookConfig } from "@storybook/angular"; -import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; import remarkGfm from "remark-gfm"; +import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; const config: StorybookConfig = { stories: [ @@ -29,6 +30,8 @@ const config: StorybookConfig = { getAbsolutePath("@storybook/addon-designs"), getAbsolutePath("@storybook/addon-interactions"), { + // @storybook/addon-docs is part of @storybook/addon-essentials + // eslint-disable-next-line storybook/no-uninstalled-addons name: "@storybook/addon-docs", options: { mdxPluginOptions: { diff --git a/.storybook/manager.js b/.storybook/manager.js index 409f93ec5055..e0ec04fd3755 100644 --- a/.storybook/manager.js +++ b/.storybook/manager.js @@ -50,10 +50,14 @@ const darkTheme = create({ }); export const getPreferredColorScheme = () => { - if (!globalThis || !globalThis.matchMedia) return "light"; + if (!globalThis || !globalThis.matchMedia) { + return "light"; + } const isDarkThemePreferred = globalThis.matchMedia("(prefers-color-scheme: dark)").matches; - if (isDarkThemePreferred) return "dark"; + if (isDarkThemePreferred) { + return "dark"; + } return "light"; }; diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b31121e17b2..295c290a37a7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "**/_locales/*[^n]/messages.json": true }, "rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"], - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "eslint.useFlatConfig": true } diff --git a/apps/browser/.eslintrc.json b/apps/browser/.eslintrc.json deleted file mode 100644 index ba960511839b..000000000000 --- a/apps/browser/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "env": { - "browser": true, - "webextensions": true - }, - "overrides": [ - { - "files": ["src/**/*.ts"], - "excludedFiles": [ - "src/**/{content,popup,spec}/**/*.ts", - "src/**/autofill/{notification,overlay}/**/*.ts", - "src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts", - "src/**/*.spec.ts" - ], - "rules": { - "no-restricted-globals": [ - "error", - { - "name": "window", - "message": "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead." - } - ] - } - } - ] -} diff --git a/apps/browser/package.json b/apps/browser/package.json index 9ad1805362e7..154543d85cc7 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,30 +1,29 @@ { "name": "@bitwarden/browser", - "version": "2025.1.1", + "version": "2025.2.0", "scripts": { "build": "npm run build:chrome", - "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", - "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 webpack", - "build:firefox": "cross-env BROWSER=firefox webpack", - "build:opera": "cross-env BROWSER=opera MANIFEST_VERSION=3 webpack", - "build:safari": "cross-env BROWSER=safari webpack", + "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:firefox": "cross-env BROWSER=firefox NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:opera": "cross-env BROWSER=opera NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:safari": "cross-env BROWSER=safari NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:watch": "npm run build:watch:chrome", "build:watch:chrome": "npm run build:chrome -- --watch", "build:watch:edge": "npm run build:edge -- --watch", "build:watch:firefox": "npm run build:firefox -- --watch", "build:watch:opera": "npm run build:opera -- --watch", "build:watch:safari": "npm run build:safari -- --watch", - "build:prod:chrome": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:chrome", - "build:prod:edge": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:edge", - "build:prod:firefox": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:firefox", - "build:prod:opera": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:opera", - "build:prod:safari": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:safari", + "build:prod:chrome": "cross-env NODE_ENV=production npm run build:chrome", + "build:prod:edge": "cross-env NODE_ENV=production npm run build:edge", + "build:prod:firefox": "cross-env NODE_ENV=production npm run build:firefox", + "build:prod:opera": "cross-env NODE_ENV=production npm run build:opera", + "build:prod:safari": "cross-env NODE_ENV=production npm run build:safari", "dist:chrome": "npm run build:prod:chrome && mkdir -p dist && ./scripts/compress.ps1 dist-chrome.zip", "dist:edge": "npm run build:prod:edge && mkdir -p dist && ./scripts/compress.ps1 dist-edge.zip", "dist:firefox": "npm run build:prod:firefox && mkdir -p dist && ./scripts/compress.ps1 dist-firefox.zip", "dist:opera": "npm run build:prod:opera && mkdir -p dist && ./scripts/compress.ps1 dist-opera.zip", "dist:safari": "npm run build:prod:safari && ./scripts/package-safari.ps1", - "dist:edge:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:edge", "dist:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:firefox", "dist:opera:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:opera", "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari", diff --git a/apps/browser/scripts/package-safari.ps1 b/apps/browser/scripts/package-safari.ps1 index 075ed6060705..ce208478098c 100755 --- a/apps/browser/scripts/package-safari.ps1 +++ b/apps/browser/scripts/package-safari.ps1 @@ -52,7 +52,7 @@ foreach ($subBuildPath in $subBuildPaths) { "--verbose", "--force", "--sign", - "E7C9978F6FBCE0553429185C405E61F5380BE8EB", + "4B9662CAB74E8E4F4ECBDD9EDEF2543659D95E3C", "--entitlements", $entitlementsPath ) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 8b4bbe23e040..50442228b7cc 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "توليد عبارة المرور" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "إعادة توليد كلمة المرور" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "قم بتأكيد هويتك" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "خزانتك مقفلة. قم بتأكيد هويتك للمتابعة." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "اطلب إضافة عنصر إذا لم يتم العثور على عنصر في المخزن الخاص بك. ينطبق على جميع حسابات تسجيل الدخول." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "أظهر البطاقات في صفحة التبويبات" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "قائمة عناصر البطاقة في صفحة التبويب لسهولة التعبئة التلقائية." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "إظهار الهويات على صفحة التبويب" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "شراء العضوية المميزة" }, - "premiumPurchaseAlert": { - "message": "يمكنك شراء العضوية المتميزة على bitwarden.com على خزانة الويب. هل تريد زيارة الموقع الآن؟" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "مولد اسم المستخدم" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "استخدم كلمة المرور هذه" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "الموقع $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "تم إرسال إشعار إلى جهازك." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "بَدْء تسجيل الدخول" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "كلمة المرور الرئيسية مكشوفة" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "استيراد بياناتك إلى Bitwarden؟", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "حماية بياناتك الأخيرة واستيرادك إلى Bitwarden؟", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "حفظ كملف غير مشفر", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "استيراد إلى Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "جارِ الاستيراد...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "لقد تم استيراد البيانات بنجاح!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "خطأ في الاستيراد. تحقق من وحدة التحكم للحصول على التفاصيل.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "تم مواجهة خطأ في الشبكة أثناء الاستيراد.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "الاسم البديل للنطاق" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 86f1d07fb3f8..5361782b104c 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Keçid ifadələri yarat" }, + "passwordGenerated": { + "message": "Parol yaradıldı." + }, + "passphraseGenerated": { + "message": "Keçid ifadəsi yaradıldı" + }, + "usernameGenerated": { + "message": "İstifadəçi adı yaradıldı" + }, + "emailGenerated": { + "message": "E-poçt yaradıldı" + }, "regeneratePassword": { "message": "Parolu yenidən yarat" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Kimliyi doğrula" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." + }, + "continueLoggingIn": { + "message": "Giriş etməyə davam" + }, "yourVaultIsLocked": { "message": "Seyfiniz kilidlənib. Davam etmək üçün kimliyinizi doğrulayın." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Seyfinizdə tapılmayan elementin əlavə edilməsi soruşulsun. Giriş etmiş bütün hesablara aiddir." }, - "showCardsInVaultView": { - "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" + "showCardsInVaultViewV2": { + "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq həmişə göstər" }, "showCardsCurrentTab": { "message": "Kartları Vərəq səhifəsində göstər" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Asan avto-doldurma üçün Vərəq səhifəsində kart elementlərini sadalayın." }, - "showIdentitiesInVaultView": { - "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" + "showIdentitiesInVaultViewV2": { + "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq həmişə göstər" }, "showIdentitiesCurrentTab": { "message": "Vərəq səhifəsində kimlikləri göstər" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium satın al" }, - "premiumPurchaseAlert": { - "message": "Premium üzvlüyü bitwarden.com veb seyfində 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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "İstifadəçi adı yaradıcı" }, + "useThisEmail": { + "message": "Bu e-poçtu istifadə et" + }, "useThisPassword": { "message": "Bu parolu istifadə et" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Bu veb saytlar üçün avto-doldurma və digər əlaqəli özəlliklər təklif olunmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." }, - "autofillBlockedNotice": { - "message": "Bu veb sayt üçün avto-doldurma əngəllənib. Bunu ayarlarda incələyin və ya dəyişdirin." + "autofillBlockedNoticeV2": { + "message": "Bu veb sayt üçün avto-doldurma əngəllənib." }, - "autofillBlockedTooltip": { - "message": "Bu veb saytda avto-doldurma əngəllənib. Ayarlarda incələyin." + "autofillBlockedNoticeGuidance": { + "message": "Bunu ayarlarda dəyişdir" }, "websiteItemLabel": { "message": "Veb sayt $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Cihazınıza bir bildiriş göndərildi." }, + "notificationSentDevicePart1": { + "message": "Cihazınızda Bitwarden kilidini açın, ya da " + }, + "notificationSentDeviceAnchor": { + "message": "veb tətbiqinizdə" + }, + "notificationSentDevicePart2": { + "message": "Təsdiqləməzdən əvvəl Barmaq izi ifadəsinin aşağıdakı ifadə ilə uyuşduğuna əmin olun." + }, "aNotificationWasSentToYourDevice": { "message": "Cihazınıza bir bildiriş göndərildi" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Hesabınızın kilidinin açıq olduğuna və barmaq izi ifadəsinin digər cihazda uyuşduğuna əmin olun" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Tələbiniz təsdiqləndikdə bildiriş alacaqsınız" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Giriş başladıldı" }, + "logInRequestSent": { + "message": "Tələb göndərildi" + }, "exposedMasterPassword": { "message": "İfşa olunmuş ana parol" }, @@ -3425,38 +3452,6 @@ "message": "Yığcamlaşdırmanı aç/bağla", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Datanız Bitwarden-ə köçürülsün?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass datanızı qoruyub Bitwarden-ə köçürürsünüz?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Şifrələnməmiş fayl olaraq saxla", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden-ə köçür", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Daxilə köçürülür...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data uğurla daxilə köçürüldü!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Daxilə köçürmə xətası. Detallar üçün konsolu yoxlayın.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Daxilə köçürmə zamanı şəbəkə xətası baş verdi.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domen ləqəbi" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Avto-doldurma təklifləri" }, + "itemSuggestions": { + "message": "Təklif olunan elementlər" + }, "autofillSuggestionsTip": { "message": "Bu saytın avto-doldurması üçün giriş elementini saxlayın" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Element adı" }, - "cannotRemoveViewOnlyCollections": { - "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Təşkilat deaktiv edildi" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Mətn \"Send\"ləri" }, - "bitwardenNewLook": { - "message": "Bitwarden-in yeni bir görünüşü var!" - }, - "bitwardenNewLookDesc": { - "message": "Seyf 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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Ekstra enli" + }, + "cannotRemoveViewOnlyCollections": { + "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lütfən masaüstü tətbiqinizi güncəlləyin" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Biometrik kilid açmanı istifadə etmək üçün lütfən masaüstü tətbiqinizi güncəlləyin, ya da masaüstü ayarlarında barmaq izi ilə kilid açmanı sıradan çıxardın." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 036b1dfcb24a..3c63e22406d4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Згенерыраваць парольную фразу" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Паўторна генерыраваць пароль" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Праверыць асобу" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Ваша сховішча заблакіравана. Каб працягнуць, пацвердзіце сваю асобу." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Паказваць карткі на старонцы з укладкамі" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Спіс элементаў картак на старонцы з укладкамі для лёгкага аўтазапаўнення." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Паказваць пасведчанні на старонцы з укладкамі" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Купіць прэміум" }, - "premiumPurchaseAlert": { - "message": "Вы можаце купіць прэміяльны статус на bitwarden.com. Перайсці на вэб-сайт зараз?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератар імені карыстальніка" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Выкарыстоўваць гэты пароль" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Вэб-сайт $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Апавяшчэнне было адпраўлена на вашу прыладу." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Ініцыяваны ўваход" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Скампраметаваны асноўны пароль" }, @@ -3425,38 +3452,6 @@ "message": "Згарнуць/Разгарнуць", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Імпартаванне...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Даныя паспяхова імпартаваны!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Мянушка дамена" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Назва элемента" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 098e2b910511..d40c4f82e3f8 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генериране на парола-фраза" }, + "passwordGenerated": { + "message": "Паролата е генерирана" + }, + "passphraseGenerated": { + "message": "Паролата-фраза е генерирана" + }, + "usernameGenerated": { + "message": "Потребителското име е генерирано" + }, + "emailGenerated": { + "message": "Е-пощата е генерирана" + }, "regeneratePassword": { "message": "Регенериране на паролата" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Потвърждаване на самоличността" }, + "weDontRecognizeThisDevice": { + "message": "Това устройство е непознато. Въведете кода изпратен на е-пощата Ви, за да потвърдите самоличността си." + }, + "continueLoggingIn": { + "message": "Продължаване с вписването" + }, "yourVaultIsLocked": { "message": "Трезорът е заключен — въведете главната си парола, за да продължите." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Питане за добавяне на елемент, ако такъв не бъде намерен в трезора. Прилага се за всички регистрации, в които сте вписан(а)." }, - "showCardsInVaultView": { - "message": "Показване на картите като предложения за авт. попълване в изгледа на трезора" + "showCardsInVaultViewV2": { + "message": "Картите да се показват винаги като предложения за авт. попълване в изгледа на трезора" }, "showCardsCurrentTab": { "message": "Показване на карти в страницата с разделите" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Показване на картите в страницата с разделите, за лесно автоматично попълване." }, - "showIdentitiesInVaultView": { - "message": "Показване на самоличности като предложения за автоматично попълване в изгледа на трезора" + "showIdentitiesInVaultViewV2": { + "message": "Самоличностите да се показват винаги като предложения за автоматично попълване в изгледа на трезора" }, "showIdentitiesCurrentTab": { "message": "Показване на самоличности в страницата с разделите" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Покупка на платен абонамент" }, - "premiumPurchaseAlert": { - "message": "Може да платите абонамента си през сайта bitwarden.com. Искате ли да го посетите сега?" - }, "premiumPurchaseAlertV2": { "message": "Можете да закупите платената версия от настройките на регистрацията си, в приложението по уеб на Битуорден." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератор на потребителски имена" }, + "useThisEmail": { + "message": "Използване на тази е-поща" + }, "useThisPassword": { "message": "Използване на тази парола" }, @@ -2285,7 +2303,7 @@ "message": "Потребителят е заключен или отписан" }, "biometricsNotUnlockedDesc": { - "message": "Отключете потребителя в настолното приложение и опитайте отново." + "message": "Отключете потребителя в самостоятелното приложение и опитайте отново." }, "biometricsNotAvailableTitle": { "message": "Отключването чрез биометрични данни не е налично" @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Автоматичното попълване и други свързани функции няма да бъдат предлагани за тези уеб сайтове. Трябва да презаредите страницата, за да влязат в сила промените." }, - "autofillBlockedNotice": { - "message": "Автоматичното попълване е блокирано за този уеб сайт. Можете да прегледате и промените това в настройките." + "autofillBlockedNoticeV2": { + "message": "Автоматичното попълване е блокирано за този уеб сайт." }, - "autofillBlockedTooltip": { - "message": "Автоматичното попълване е блокирано за този уеб сайт. Можете да прегледате това в настройките." + "autofillBlockedNoticeGuidance": { + "message": "Променете това в настройките" }, "websiteItemLabel": { "message": "Уеб сайт $number$ (адрес)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Към устройството Ви е изпратено известие." }, + "notificationSentDevicePart1": { + "message": "Отключете Битоурден на устройството си или в" + }, + "notificationSentDeviceAnchor": { + "message": "приложението по уеб" + }, + "notificationSentDevicePart2": { + "message": "Уверете се, че уникалната фраза съвпада с тази по-долу, преди да одобрите." + }, "aNotificationWasSentToYourDevice": { "message": "Към устройството Ви е изпратено известие" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Уверете се, че регистрацията Ви е отключена и че уникалната фраза съвпада с другото устройство" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Ще получите уведомление когато заявката бъде одобрена" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Вписването е стартирано" }, + "logInRequestSent": { + "message": "Заявката е изпратена" + }, "exposedMasterPassword": { "message": "Разобличена главна парола" }, @@ -3425,38 +3452,6 @@ "message": "Превключване на свиването", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Внасяне на данните Ви в Битуорден?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Защитаване на данните Ви от LastPass и внасяне в Битуорден?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Запазване като нешифрован файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Внасяне в Битуорден", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Внасяне…", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Данните бяха внесени успешно!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Грешка при внасянето. Вижте конзолата за подробности.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "По време на внасянето възникна мрежова грешка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдонимен домейн" }, @@ -4008,7 +4003,10 @@ "message": "Секретният ключ е премахнат" }, "autofillSuggestions": { - "message": "Автоматично попълване на предложения" + "message": "Предложения за авт. попълване" + }, + "itemSuggestions": { + "message": "Препоръчани елементи" }, "autofillSuggestionsTip": { "message": "Запазване на елемент за вписване за този уеб сайт, за авт. попълване" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Име на елемента" }, - "cannotRemoveViewOnlyCollections": { - "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Организацията е деактивирана" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Текстови изпращания" }, - "bitwardenNewLook": { - "message": "Биуорден има нов облик!" - }, - "bitwardenNewLookDesc": { - "message": "Сега е по-лесно и интуитивно от всякога да използвате автоматичното попълване и да търсите в раздела на трезора. Разгледайте!" - }, "accountActions": { "message": "Действия по регистрацията" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Много широко" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Моля, обновете самостоятелното приложение" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "За да използвате отключването чрез биометрични данни, обновете самостоятелното приложение или изключете отключването чрез пръстов отпечатък в настройките му." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index f60fc2c96838..6de7161004b0 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "পাসওয়ার্ড পুনঃতৈরি করুন" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "আপনার ভল্ট লক করা আছে। চালিয়ে যেতে আপনার মূল পাসওয়ার্ডটি যাচাই করান।" }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "প্রিমিয়াম কিনুন" }, - "premiumPurchaseAlert": { - "message": "আপনি bitwarden.com ওয়েব ভল্টে প্রিমিয়াম সদস্যতা কিনতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index aaf04da98b7d..f2c3c1c0002a 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index e7113d5aab1a..cb5fc9fb61af 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -20,16 +20,16 @@ "message": "Crea un compte" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nou a Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Inicieu sessió amb la clau de pas" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Inici de sessió únic" }, "welcomeBack": { - "message": "Welcome back" + "message": "Benvingut/da de nou" }, "setAStrongPassword": { "message": "Estableix una contrasenya segura" @@ -120,7 +120,7 @@ "message": "Copia contrasenya" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Copia frase de pas" }, "copyNote": { "message": "Copia nota" @@ -153,13 +153,13 @@ "message": "Copia el número de llicència" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Copia la clau privada" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Copieu la clau pública" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Copia l'empremta digital" }, "copyCustomField": { "message": "Copia $FIELD$", @@ -177,7 +177,7 @@ "message": "Copia notes" }, "fill": { - "message": "Fill", + "message": "Emplena", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "Emplena automàticament l'identitat" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Emplena el codi de verificació" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Emplena el codi de verificació", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -443,7 +443,19 @@ "message": "Genera contrasenya" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Genera frase de pas" + }, + "passwordGenerated": { + "message": "Contrasenya generada" + }, + "passphraseGenerated": { + "message": "Frase de pas generada" + }, + "usernameGenerated": { + "message": "Nom d'usuari generat" + }, + "emailGenerated": { + "message": "Correu electrònic generat" }, "regeneratePassword": { "message": "Regenera contrasenya" @@ -530,7 +542,7 @@ "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "Els requisits de la política empresarial s'han aplicat a les opcions del generador.", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -576,7 +588,7 @@ "message": "Notes" }, "privateNote": { - "message": "Private note" + "message": "Nota privada" }, "note": { "message": "Nota" @@ -600,7 +612,7 @@ "message": "Obri la web" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Inicia el lloc web $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifica identitat" }, + "weDontRecognizeThisDevice": { + "message": "No reconeixem aquest dispositiu. Introduïu el codi que us hem enviat al correu electrònic per verificar la identitat." + }, + "continueLoggingIn": { + "message": "Continua l'inici de sessió" + }, "yourVaultIsLocked": { "message": "La caixa forta està bloquejada. Comproveu la contrasenya mestra per continuar." }, @@ -682,7 +700,7 @@ "message": "Temps d'espera de la caixa forta" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Temps d'espera" }, "lockNow": { "message": "Bloqueja ara" @@ -779,10 +797,10 @@ "message": "El vostre compte s'ha creat correctament. Ara ja podeu iniciar sessió." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "S'ha creat el vostre compte nou!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Heu iniciat sessió!" }, "youSuccessfullyLoggedIn": { "message": "Heu iniciat sessió correctament" @@ -831,10 +849,10 @@ "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." + "message": "Bitwarden pot emmagatzemar i omplir codis de verificació en dos passos. Seleccioneu la icona de la càmera per fer una captura de pantalla del codi QR de l'autenticador d'aquest lloc web o copieu i enganxeu la clau en aquest camp." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Més informació sobre els autenticadors" }, "copyTOTP": { "message": "Copia la clau de l'autenticador (TOTP)" @@ -843,7 +861,7 @@ "message": "Sessió tancada" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Heu tancat la sessió del compte." }, "loginExpired": { "message": "La vostra sessió ha caducat." @@ -852,19 +870,19 @@ "message": "Inicia sessió" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Inicia sessió a Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "Reinicia el registre" }, "expiredLink": { - "message": "Expired link" + "message": "Enllaç caducat" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "Reinicieu el registre o proveu d'iniciar sessió." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "És possible que ja tingueu un compte" }, "logOutConfirmation": { "message": "Segur que voleu tancar la sessió?" @@ -888,10 +906,10 @@ "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Fes que el vostre compte siga més segur configurant l'inici de sessió en dos passos a l'aplicació web de Bitwarden." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Continua cap a l'aplicació web?" }, "editedFolder": { "message": "Carpeta guardada" @@ -978,7 +996,7 @@ "message": "Demana d'afegir els inicis de sessió" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "Opcions de guardar a la caixa forta" }, "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." @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Mostra sempre les targetes com a suggeriments d'emplenament automàtic a la vista de la caixa forta" }, "showCardsCurrentTab": { "message": "Mostra les targetes a la pàgina de pestanya" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Llista els elements de la targeta a la pàgina de pestanya per facilitar l'autoemplenat." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Mostra sempre les identitats com a suggeriments d'emplenament automàtic a la vista de la caixa forta" }, "showIdentitiesCurrentTab": { "message": "Mostra les identitats a la pàgina de pestanya" @@ -1005,7 +1023,7 @@ "message": "Llista els elements d'identitat de la pestanya de la pàgina per facilitar l'autoemplenat." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Feu clic als elements per emplenar automàticament a la vista de la caixa forta" }, "clearClipboard": { "message": "Buida el porta-retalls", @@ -1098,7 +1116,7 @@ "message": "Format de fitxer" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Aquesta exportació de fitxers estarà protegida amb contrasenya i requerirà la contrasenya del fitxer per desxifrar-la." }, "filePassword": { "message": "Contrasenya del fitxer" @@ -1122,11 +1140,11 @@ "message": "\"Contrasenya del fitxer\" i \"Confirma contrasenya del fitxer\" no coincideixen." }, "warning": { - "message": "ADVERTIMENT", + "message": "AVÍS", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Advertència", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1148,7 +1166,7 @@ "message": "Compartit" }, "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 per empreses us permet compartir els elements de la vostra caixa forta amb altres usuaris mitjançant una organització. Més informació al lloc web bitwarden.com." }, "moveToOrganization": { "message": "Desplaça a l'organització" @@ -1206,7 +1224,7 @@ "message": "Fitxer" }, "fileToShare": { - "message": "File to share" + "message": "Fitxer per compartir" }, "selectFile": { "message": "Seleccioneu un fitxer" @@ -1242,7 +1260,7 @@ "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "Accés d’emergència." }, "premiumSignUpTwoStepOptions": { "message": "Opcions propietàries de doble factor com ara YubiKey i Duo." @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Compra Premium" }, - "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." }, @@ -1287,7 +1302,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "Tot per només per $PRICE$ a l'any!", "placeholders": { "price": { "content": "$1", @@ -1317,10 +1332,10 @@ "message": "Introduïu el codi de verificació de 6 dígits de l'aplicació autenticadora." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Temps d'espera d'autenticació" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "La sessió d'autenticació s'ha esgotat. Reinicieu el procés d'inici de sessió." }, "enterVerificationCodeEmail": { "message": "Introduïu el codi de verificació de 6 dígits que s'ha enviat per correu electrònic a $EMAIL$.", @@ -1390,7 +1405,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." @@ -1413,7 +1428,7 @@ "message": "Correu electrònic" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Introduïu el codi que us hem enviat al correu electrònic." }, "selfHostedEnvironment": { "message": "Entorn d'allotjament propi" @@ -1425,7 +1440,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." @@ -1440,7 +1455,7 @@ "message": "URL del servidor" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "URL del servidor autoallotjat", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1466,10 +1481,10 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Suggeriments d'emplenament automàtic" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Mostra suggeriments d'emplenament automàtic als camps del formulari" }, "showInlineMenuIdentitiesLabel": { "message": "Display identities as suggestions" @@ -1478,10 +1493,10 @@ "message": "Display cards as suggestions" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Mostra suggeriments quan la icona està seleccionada" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "S'aplica a tots els comptes connectats." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Desactiveu la configuració integrada del gestor de contrasenyes del vostre navegador per evitar conflictes." @@ -1502,7 +1517,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "Emplenament automàtic a la càrrega de la pàgina" }, "enableAutoFillOnPageLoad": { "message": "Habilita l'emplenament automàtic en carregar la pàgina" @@ -1514,7 +1529,7 @@ "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" + "message": "Més informació sobre riscs" }, "learnMoreAboutAutofill": { "message": "Obteniu més informació sobre l'emplenament automàtic" @@ -1544,13 +1559,13 @@ "message": "Obri la caixa forta a la barra lateral" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Emplenament automàtic amb l'últim inici de sessió utilitzat per al lloc web actual" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Emplenament automàtic amb l'última targeta utilitzada per al lloc web actual" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Emplenament automàtic amb l'última identitat utilitzada per al lloc web actual" }, "commandGeneratePasswordDesc": { "message": "Genera i copia una nova contrasenya aleatòria al porta-retalls." @@ -1768,10 +1783,10 @@ "message": "Identitat" }, "typeSshKey": { - "message": "SSH key" + "message": "Clau SSH" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Nou $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1780,7 +1795,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Edita $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1789,7 +1804,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Mostra $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1801,13 +1816,13 @@ "message": "Historial de les contrasenyes" }, "generatorHistory": { - "message": "Generator history" + "message": "Historial del generador" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Neteja l'historial del generador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Si continueu, totes les entrades se suprimiran permanentment de l'historial del generador. Esteu segur que voleu continuar?" }, "back": { "message": "Arrere" @@ -1816,7 +1831,7 @@ "message": "Col·leccions" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ col·leccions", "placeholders": { "count": { "content": "$1", @@ -1846,7 +1861,7 @@ "message": "Notes segures" }, "sshKeys": { - "message": "SSH Keys" + "message": "Claus SSH" }, "clear": { "message": "Esborra", @@ -1872,7 +1887,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Domini base (recomanat)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1926,10 +1941,10 @@ "message": "No hi ha cap contrasenya a llistar." }, "clearHistory": { - "message": "Clear history" + "message": "Neteja l'historial" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Res a mostrar" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -1993,10 +2008,10 @@ "message": "Desbloqueja amb codi PIN" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "Estableix el PIN" }, "setYourPinButton": { - "message": "Set PIN" + "message": "Estableix el PIN" }, "setYourPinCode": { "message": "Configureu el vostre codi PIN per desbloquejar Bitwarden. La configuració del PIN es restablirà si tanqueu la sessió definitivament." @@ -2017,7 +2032,7 @@ "message": "Desbloqueja amb biomètrica" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Desbloqueja amb contrasenya mestra" }, "awaitDesktop": { "message": "S’espera confirmació des de l’escriptori" @@ -2029,7 +2044,7 @@ "message": "Bloqueja amb la contrasenya mestra en reiniciar el navegador" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Sol·licita la contrasenya mestra en reiniciar el navegador" }, "selectOneCollection": { "message": "Heu d'escollir com a mínim una col·lecció." @@ -2041,16 +2056,19 @@ "message": "Clona" }, "passwordGenerator": { - "message": "Password generator" + "message": "Generador de contrasenyes" }, "usernameGenerator": { - "message": "Username generator" + "message": "Generador de nom d'usuari" + }, + "useThisEmail": { + "message": "Utilitza aquest correu" }, "useThisPassword": { - "message": "Use this password" + "message": "Utilitzeu aquesta contrasenya" }, "useThisUsername": { - "message": "Use this username" + "message": "Utilitzeu aquest nom d'usuari" }, "securePasswordGenerated": { "message": "Secure password generated! Don't forget to also update your password on the website." @@ -2096,7 +2114,7 @@ "message": "Element restaurat" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Ja teniu un compte?" }, "vaultTimeoutLogOutConfirmation": { "message": "En tancar la sessió s'eliminarà tot l'accés a la vostra caixa forta i es requerirà una autenticació en línia després del període de temps d'espera. Esteu segur que voleu utilitzar aquesta configuració?" @@ -2108,7 +2126,7 @@ "message": "Ompli automàticament i guarda" }, "fillAndSave": { - "message": "Fill and save" + "message": "Ompli i guarda" }, "autoFillSuccessAndSavedUri": { "message": "Element emplenat automàticament i URI guardat" @@ -2195,10 +2213,10 @@ "message": "Anul·la subscripció" }, "atAnyTime": { - "message": "at any time." + "message": "en qualsevol moment." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "En continuar, acceptes el" }, "and": { "message": "i" @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Dominis bloquejats" }, "excludedDomains": { "message": "Dominis exclosos" @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Canvieu-ho a la configuració" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -2391,7 +2409,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Detalls de l'enviament", + "message": "Detalls del Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -2408,7 +2426,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Ocultar el text per defecte" + "message": "Amaga el text per defecte" }, "expired": { "message": "Caducat" @@ -2455,7 +2473,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "Esteu segur que voleu suprimir permanentment aquest Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { @@ -2466,7 +2484,7 @@ "message": "Data de supressió" }, "deletionDateDescV2": { - "message": "L'enviament s'esborrarà permanentment en aquesta data.", + "message": "El Send se suprimirà permanentment en aquesta data.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { @@ -2511,7 +2529,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "Send creat correctament!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { @@ -2604,7 +2622,7 @@ "message": "Es requereix verificació del correu electrònic" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Correu electrònic verificat" }, "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." @@ -2646,7 +2664,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "de $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2790,10 +2808,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "S'està exportant la caixa forta de l’organització" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Només s'exportarà la caixa forta de l'organització associada a $ORGANIZATION$. No s'inclouran els elements de les caixes fortes individuals ni d'altres organitzacions.", "placeholders": { "organization": { "content": "$1", @@ -2805,27 +2823,27 @@ "message": "Error" }, "decryptionError": { - "message": "Decryption error" + "message": "Error de desxifrat" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden no ha pogut desxifrar els elements de la caixa forta que s'indiquen a continuació." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Contacteu amb el servei d'atenció al client", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "per evitar la pèrdua de dades addicionals.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Genera un nom d'usuari" }, "generateEmail": { - "message": "Generate email" + "message": "Genera correu electrònic" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "El valor ha d'estar entre $MIN$ i $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2839,7 +2857,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Utilitzeu $RECOMMENDED$ caràcters o més per generar una contrasenya segura.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2849,7 +2867,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Utilitzeu $RECOMMENDED$ paraules o més per generar una frase de pas segura.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2890,7 +2908,7 @@ "message": "Genera un àlies de correu electrònic amb un servei de reenviament extern." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Domini del correu electrònic", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -3097,29 +3115,38 @@ "message": "Torna a enviar la notificació" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Veure totes les opcions d'inici de sessió" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Veure totes les opcions d'inici de sessió" }, "notificationSentDevice": { "message": "S'ha enviat una notificació al vostre dispositiu." }, - "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "aNotificationWasSentToYourDevice": { + "message": "S'ha enviat una notificació al vostre dispositiu" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Se us notificarà un vegada s'haja aprovat la sol·licitud" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Necessiteu una altra opció?" }, "loginInitiated": { "message": "S'ha iniciat la sessió" }, + "logInRequestSent": { + "message": "Sol·licitud enviada" + }, "exposedMasterPassword": { "message": "Contrasenya mestra exposada" }, @@ -3175,22 +3202,22 @@ "message": "Configuració d'emplenament automàtic" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "Drecera d'emplenament automàtic" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "Canvia la drecera" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Gestiona les dreceres" }, "autofillShortcut": { "message": "Drecera de teclat d'emplenament automàtic" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "La drecera d'inici de sessió no està configurada. Canvieu-ho a la configuració del navegador." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "La drecera d'emplenament automàtic és $COMMAND$. Gestioneu totes les dreceres a la configuració del navegador.", "placeholders": { "command": { "content": "$1", @@ -3241,25 +3268,25 @@ "message": "Es requereix un identificador SSO de l'organització." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Creant compte en" }, "checkYourEmail": { - "message": "Check your email" + "message": "Comprova el 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." + "message": "i continua creant el compte." }, "noEmail": { - "message": "No email?" + "message": "Sense correu electrònic?" }, "goBack": { "message": "Torna arrere" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "per editar l'adreça de correu electrònic." }, "eu": { "message": "UE", @@ -3302,11 +3329,11 @@ "message": "Dispositiu de confiança" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "No hi ha Sends actius", "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.", + "message": "Utilitzeu Send per compartir informació xifrada de manera segura amb qualsevol persona.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3383,10 +3410,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 camp necessita la vostra atenció." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ camps necessiten la vostra atenció.", "placeholders": { "count": { "content": "$1", @@ -3425,38 +3452,6 @@ "message": "Redueix/Amplia", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Voleu importar les vostres dades a Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protegiu les vostres dades de LastPass i importeu-les a Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guarda com a fitxer sense xifrar", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importa a Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "S'està important...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Les dades s'han importat correctament!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "S'ha produït un error en importar. Consulteu la consola per obtenir més informació.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "S'ha trobat un error de xarxa durant la importació.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alies de domini" }, @@ -3473,7 +3468,7 @@ "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": "Canvia a la navegació lateral" }, "skipToContent": { "message": "Vés al contingut" @@ -3535,7 +3530,7 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "Inici de sessió nou", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { @@ -3551,7 +3546,7 @@ "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "Identitat nova", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { @@ -3645,7 +3640,7 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "S'ha produït un error en connectar amb el servei Duo. Utilitzeu un mètode d'inici de sessió en dos passos diferent o poseu-vos en contacte amb Duo per obtenir ajuda." }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Inicieu DUO i seguiu els passos per finalitzar la sessió." @@ -3740,10 +3735,10 @@ "message": "Clau de pas" }, "accessing": { - "message": "Accessing" + "message": "Accedint a" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Connectat!" }, "passkeyNotCopied": { "message": "La clau de pas no es copiarà" @@ -3755,7 +3750,7 @@ "message": "Verificació requerida pel lloc iniciador. Aquesta funció encara no s'ha implementat per als comptes sense contrasenya mestra." }, "logInWithPasskeyQuestion": { - "message": "Log in with passkey?" + "message": "Inici de sessió amb clau de pas?" }, "passkeyAlreadyExists": { "message": "Ja hi ha una clau de pas per a aquesta aplicació." @@ -3770,7 +3765,7 @@ "message": "No matching logins for this site" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Cerca o guarda la clau de pas com a nou inici de sessió" }, "confirm": { "message": "Confirma-ho" @@ -3782,7 +3777,7 @@ "message": "Guarda la clau de pas com a nou inici de sessió" }, "chooseCipherForPasskeySave": { - "message": "Choose a login to save this passkey to" + "message": "Trieu un inici de sessió per guardar aquesta clau de pas" }, "chooseCipherForPasskeyAuth": { "message": "Choose a passkey to log in with" @@ -3931,11 +3926,11 @@ "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "Voleu continuar a la configuració del navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "Voleu continuar cap al Centre d'ajuda?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { @@ -3983,7 +3978,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Contrasenya guardada!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { @@ -3991,7 +3986,7 @@ "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Contrasenya actualitzada!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -4008,22 +4003,25 @@ "message": "Clau de pas suprimida" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Suggeriments d'emplenament automàtic" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "La caixa forta està buida" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "No s'ha trobat cap element que coincidisca amb la cerca" }, "clearFiltersOrTryAnother": { "message": "Clear filters or try another search term" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Copia info - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4043,7 +4041,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Més opcions, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4053,7 +4051,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Més opcions - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4086,28 +4084,28 @@ "message": "No values to copy" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Assigna a col·leccions" }, "copyEmail": { - "message": "Copy email" + "message": "Copia el correu electrònic" }, "copyPhone": { - "message": "Copy phone" + "message": "Copia telèfon" }, "copyAddress": { - "message": "Copy address" + "message": "Copia l'adreça" }, "adminConsole": { "message": "Consola d'administració" }, "accountSecurity": { - "message": "Account security" + "message": "Seguretat del compte" }, "notifications": { - "message": "Notifications" + "message": "Notificacions" }, "appearance": { - "message": "Appearance" + "message": "Aparença" }, "errorAssigningTargetCollection": { "message": "S'ha produït un error en assignar la col·lecció de destinació." @@ -4116,7 +4114,7 @@ "message": "S'ha produït un error en assignar la carpeta de destinació." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Mostra elements en $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4126,7 +4124,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Torna a $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4139,7 +4137,7 @@ "message": "Nou" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Suprimeix $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nom d'element" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4183,7 +4172,7 @@ "message": "Informació addicional" }, "itemHistory": { - "message": "Item history" + "message": "Historial d'elements" }, "lastEdited": { "message": "Última edició" @@ -4195,19 +4184,19 @@ "message": "Enllaçat" }, "copySuccessful": { - "message": "Copy Successful" + "message": "Còpia correcta" }, "upload": { - "message": "Upload" + "message": "Puja" }, "addAttachment": { - "message": "Add attachment" + "message": "Afegeix adjunt" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "La mida màxima del fitxer és de 500 MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Suprimeix adjunt $NAME$", "placeholders": { "name": { "content": "$1", @@ -4225,7 +4214,7 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Esteu segur que voleu suprimir definitivament aquest adjunt?" }, "premium": { "message": "Premium" @@ -4237,7 +4226,7 @@ "message": "Filtres" }, "filterVault": { - "message": "Filter vault" + "message": "Filtra dades" }, "filterApplied": { "message": "One filter applied" @@ -4333,16 +4322,16 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Habilita l'emplenament automàtic en carregar la pàgina?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Targeta de crèdit caducada" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" }, "cardDetails": { - "message": "Card details" + "message": "Dades de la targeta" }, "cardBrandDetails": { "message": "$BRAND$ details", @@ -4357,7 +4346,7 @@ "message": "Enable animations" }, "showAnimations": { - "message": "Show animations" + "message": "Mostra animacions" }, "addAccount": { "message": "Afig compte" @@ -4369,15 +4358,15 @@ "message": "Dades" }, "passkeys": { - "message": "Passkeys", + "message": "Claus de pas", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "Contrasenyes", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "Inicieu sessió amb la clau de pas", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4522,13 +4511,13 @@ } }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Col·leccions assignades correctament" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "No heu seleccionat res." }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "Els elements seleccionats s'han desplaçat a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4537,7 +4526,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "S'han desplaçat elements a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4546,7 +4535,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "S'ha desplaçat un element a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4575,49 +4564,43 @@ "message": "Item Location" }, "fileSend": { - "message": "File Send" + "message": "Send de fitxer" }, "fileSends": { - "message": "File Sends" + "message": "Sends de fitxer" }, "textSend": { - "message": "Text Send" + "message": "Send de text" }, "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!" + "message": "Sends de text" }, "accountActions": { "message": "Account actions" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "Mostra el nombre de suggeriments d'emplenament automàtic d'inici de sessió a la icona d'extensió" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Mostra accions de còpia ràpida a la caixa forta" }, "systemDefault": { - "message": "System default" + "message": "Per defecte del sistema" }, "enterprisePolicyRequirementsApplied": { - "message": "Enterprise policy requirements have been applied to this setting" + "message": "Els requisits de la política empresarial s'han aplicat a aquesta configuració" }, "sshPrivateKey": { - "message": "Private key" + "message": "Clau privada" }, "sshPublicKey": { - "message": "Public key" + "message": "Clau pública" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Empremta digital" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipus de clau" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4632,10 +4615,10 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "Torneu-ho a provar" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "El temps d'espera personalitzat mínim és d'1 minut." }, "additionalContentAvailable": { "message": "Additional content is available" @@ -4644,10 +4627,10 @@ "message": "File saved to device. Manage from your device downloads." }, "showCharacterCount": { - "message": "Show character count" + "message": "Mostra el recompte de caràcters" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "Amaga el recompte de caràcters" }, "itemsInTrash": { "message": "Items in trash" @@ -4659,16 +4642,16 @@ "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" + "message": "Els elements que porten més de 30 dies a la paperera se suprimiran automàticament" }, "restore": { - "message": "Restore" + "message": "Restaura" }, "deleteForever": { - "message": "Delete forever" + "message": "Suprimeix per sempre" }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "No tens permisos per editar aquest fitxer" }, "biometricsStatusHelptextUnlockNeeded": { "message": "Biometric unlock is unavailable because PIN or password unlock is required first." @@ -4698,14 +4681,14 @@ "message": "Biometric unlock is currently unavailable for an unknown reason." }, "authenticating": { - "message": "Authenticating" + "message": "S'està autenticant" }, "fillGeneratedPassword": { "message": "Fill generated password", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "Contrasenya regenerada", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { @@ -4713,11 +4696,11 @@ "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Espai", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "Accent", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { @@ -4725,7 +4708,7 @@ "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Signe d'exclamació", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { @@ -4737,7 +4720,7 @@ "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Signe del dòlar", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { @@ -4753,15 +4736,15 @@ "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Asterisc", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Parèntesi esquerre", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Parèntesi dret", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { @@ -4801,7 +4784,7 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Barra Invertida", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { @@ -4809,7 +4792,7 @@ "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Punt i coma", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { @@ -4821,23 +4804,23 @@ "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Menor que", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Major que", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Coma", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Punt", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Signe d'interrogació", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { @@ -4845,22 +4828,22 @@ "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Minúscules" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Majúscules" }, "generatedPassword": { - "message": "Generated password" + "message": "Contrasenya generada" }, "compactMode": { - "message": "Compact mode" + "message": "Mode compacte" }, "beta": { "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Noticia important" }, "setupTwoStepLogin": { "message": "Set up two-step login" @@ -4872,7 +4855,7 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Recorda-m'ho més tard" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -4884,13 +4867,13 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "No, jo no" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { "message": "Yes, I can reliably access my email" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Activa l'inici de sessió en dos passos" }, "changeAcctEmail": { "message": "Change account email" @@ -4899,9 +4882,24 @@ "message": "Extension width" }, "wide": { - "message": "Wide" + "message": "Ample" }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index eb2f9149daf0..025e20285374 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Vygenerovat heslovou frázi" }, + "passwordGenerated": { + "message": "Heslo bylo vygenerováno" + }, + "passphraseGenerated": { + "message": "Heslová fráze byla vygenerována" + }, + "usernameGenerated": { + "message": "Uživatelské jméno bylo vygenerováno" + }, + "emailGenerated": { + "message": "E-mail byl vygenerován" + }, "regeneratePassword": { "message": "Vygenerovat jiné heslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Ověřit identitu" }, + "weDontRecognizeThisDevice": { + "message": "Toto zařízení nepoznáváme. Zadejte kód zaslaný na Váš e-mail pro ověření Vaší totožnosti." + }, + "continueLoggingIn": { + "message": "Pokračovat v přihlášení" + }, "yourVaultIsLocked": { "message": "Váš trezor je uzamčen. Pro pokračování musíte zadat hlavní heslo." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Vždy zobrazit karty jako návrhy automatického vyplňování v zobrazení trezoru" }, "showCardsCurrentTab": { "message": "Zobrazit platební karty na obrazovce Karta" @@ -995,8 +1013,8 @@ "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" + "showIdentitiesInVaultViewV2": { + "message": "Vždy zobrazit identity jako návrhy automatického vyplňování v zobrazení trezoru" }, "showIdentitiesCurrentTab": { "message": "Zobrazit identity na obrazovce Karta" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Zakoupit členství Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generátor uživatelského jména" }, + "useThisEmail": { + "message": "Použít tento e-mail" + }, "useThisPassword": { "message": "Použít toto heslo" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Automatické vyplňování a další související funkce nebudou pro tyto webové stránky nabízeny. Aby se změny projevily, musíte stránku aktualizovat." }, - "autofillBlockedNotice": { - "message": "Automatické vyplňování je pro tento web zablokováno. Zkontrolujte to nebo to změňte v nastavení." + "autofillBlockedNoticeV2": { + "message": "Automatické vyplňování je pro tuto stránku zablokováno." }, - "autofillBlockedTooltip": { - "message": "Automatické vyplňování je pro tento web zablokováno. Zkontrolujte to v nastavení." + "autofillBlockedNoticeGuidance": { + "message": "Změňte to v nastavení" }, "websiteItemLabel": { "message": "Webová stránka $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Na Vaše zařízení bylo odesláno oznámení." }, + "notificationSentDevicePart1": { + "message": "Odemknout Bitwarden na Vašem zařízení nebo na" + }, + "notificationSentDeviceAnchor": { + "message": "webová aplikace" + }, + "notificationSentDevicePart2": { + "message": "Před schválením se ujistěte, že fráze otisku prstu odpovídá frázi níže." + }, "aNotificationWasSentToYourDevice": { "message": "Na Vaše zařízení bylo odesláno oznámení" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Ujistěte se, že je Váš trezor odemčen a fráze otisku prstu se shodují s druhým zařízením" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Budete upozorněni, jakmile bude žádost schválena" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Bylo zahájeno přihlášení" }, + "logInRequestSent": { + "message": "Požadavek odeslán" + }, "exposedMasterPassword": { "message": "Odhalené hlavní heslo" }, @@ -3425,38 +3452,6 @@ "message": "Přepnout sbalení", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importovat data do Bitwardenu?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Chránit Vaše data LastPass a importovat do Bitwardenu?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Uložit jako nešifrovaný soubor", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importovat do Bitwardenu", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importování...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data byla úspěšně importována!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Chyba při importu. Podrobnosti naleznete v konzoli.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Při importu došlo k chybě sítě.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Doména aliasu" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Návrhy automatického vyplňování" }, + "itemSuggestions": { + "message": "Navrhované položky" + }, "autofillSuggestionsTip": { "message": "Uložit přihlašovací údaje pro tuto stránku do automatického vyplňování" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Název položky" }, - "cannotRemoveViewOnlyCollections": { - "message": "Nemůžete odebrat kolekce s oprávněními jen pro zobrazení: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organizace je deaktivována" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra široký" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Nemůžete odebrat kolekce s oprávněními jen pro zobrazení: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aktualizujte aplikaci pro stolní počítač" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Chcete-li použít biometrické odemknutí, aktualizujte aplikaci pro stolní počítač nebo v nastavení vypněte odemknutí otiskem prstů." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 8ad494f0bfbf..12485e20120d 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -29,7 +29,7 @@ "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Croeso nôl" }, "setAStrongPassword": { "message": "Gosod cyfrinair cryf" @@ -171,7 +171,7 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copïo'r wefan" }, "copyNotes": { "message": "Copy notes" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Ailgynhyrchu cyfrinair" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Gwirio'ch hunaniaeth" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Mae eich cell ar glo. Gwiriwch eich hunaniaeth i barhau." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Prynu aelodaeth uwch" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Hoffech chi fewnforio'ch data i Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Yn mewnforio...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Cafodd y data ei fewnforio'n llwyddiannus!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4104,7 +4102,7 @@ "message": "Account security" }, "notifications": { - "message": "Notifications" + "message": "Hysbysiadau" }, "appearance": { "message": "Golwg" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 9008049e1a45..9a5e61d6daa0 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generér adgangssætning" }, + "passwordGenerated": { + "message": "Adgangskode genereret" + }, + "passphraseGenerated": { + "message": "Adgangssætning genereret" + }, + "usernameGenerated": { + "message": "Brugernavn genereret" + }, + "emailGenerated": { + "message": "E-mail genereret" + }, "regeneratePassword": { "message": "Regenerér adgangskode" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Bekræft identitet" }, + "weDontRecognizeThisDevice": { + "message": "Denne enhed er ikke genkendt. Angiv koden i den tilsendte e-mail for at bekræfte identiteten." + }, + "continueLoggingIn": { + "message": "Fortsæt med at logge ind" + }, "yourVaultIsLocked": { "message": "Din boks er låst. Bekræft din identitet for at fortsætte." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Vis altid kort som Autoudfyldningsforslag ved Boks-visning" }, "showCardsCurrentTab": { "message": "Vis kort på fanebladet" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Vis kortemner på siden Fane for nem autoudfyldning." }, - "showIdentitiesInVaultView": { - "message": "Vis identiteter som Autoudfyldningsforslag ved Boks-visning" + "showIdentitiesInVaultViewV2": { + "message": "Vis altid identiteter som Autoudfyldningsforslag ved Boks-visning" }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanebladet" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Køb premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Brugernavngenerator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Anvend denne adgangskode" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofyldning og andre relaterede funktioner tilbydes ikke på disse websteder. Siden skal opdateres for at effektuere ændringerne." }, - "autofillBlockedNotice": { - "message": "Autoudfyldning er blokeret på dette websted. Gennemgå eller ændr dette i Indstillinger." + "autofillBlockedNoticeV2": { + "message": "Autoudfyldning blokeret for dette websted." }, - "autofillBlockedTooltip": { - "message": "Autoudfyldning er blokeret på dette websted. Gennemgå i Indstillinger." + "autofillBlockedNoticeGuidance": { + "message": "Ændr dette i Indstillinger" }, "websiteItemLabel": { "message": "Websted $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "En notifikation er sendt til din enhed." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "En notifikation er sendt til enheden" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Sørg for, at boksen er oplåst, samt at fingeraftrykssætningen matcher på den anden enhed" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Man vil få besked, når anmodningen er godkendt" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Indlogning påbegyndt" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Kompromitteret hovedadgangskode" }, @@ -3425,38 +3452,6 @@ "message": "Fold sammen/ud", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importér data til Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Beskyt LastPass-data og importér dem til Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Gem som ukrypteret fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importér til Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerer...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data er nu importeret!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fejl under import. Tjek konsollen for detaljer.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netværksfejl opstod under import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomæne" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autoudfyldningsforslag" }, + "itemSuggestions": { + "message": "Foreslåede emner" + }, "autofillSuggestionsTip": { "message": "Gem et loginemne for dette websted til autoudfyldning" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Emnenavn" }, - "cannotRemoveViewOnlyCollections": { - "message": "Samlinger med kun tilladelsen Vis kan ikke fjernes: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organisation er deaktiveret" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Ekstra bred" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Samlinger med kun tilladelsen Vis kan ikke fjernes: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Opdatér venligst computerapplikationen" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "For brug af biometrisk oplåsning skal computerapplikationen opdateres eller fingeraftryksoplåsning deaktiveres i computerindstillingerne." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index fce84d1b4317..23f35fc931ce 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Passphrase generieren" }, + "passwordGenerated": { + "message": "Passwort generiert" + }, + "passphraseGenerated": { + "message": "Passphrase generiert" + }, + "usernameGenerated": { + "message": "Benutzername generiert" + }, + "emailGenerated": { + "message": "E-Mail-Adresse generiert" + }, "regeneratePassword": { "message": "Passwort neu generieren" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identität verifizieren" }, + "weDontRecognizeThisDevice": { + "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." + }, + "continueLoggingIn": { + "message": "Anmeldung fortsetzen" + }, "yourVaultIsLocked": { "message": "Dein Tresor ist gesperrt. Verifiziere deine Identität, um fortzufahren." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Karten immer als Auto-Ausfüllen-Vorschläge in der Tresor-Ansicht anzeigen" }, "showCardsCurrentTab": { "message": "Karten auf Tab Seite anzeigen" @@ -995,8 +1013,8 @@ "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" + "showIdentitiesInVaultViewV2": { + "message": "Identitäten immer als Auto-Ausfüllen-Vorschläge in der Tresor-Ansicht anzeigen" }, "showIdentitiesCurrentTab": { "message": "Identitäten auf Tab Seite anzeigen" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium-Mitgliedschaft kaufen" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Benutzernamen-Generator" }, + "useThisEmail": { + "message": "Diese E-Mail-Adresse verwenden" + }, "useThisPassword": { "message": "Dieses Passwort verwenden" }, @@ -2337,13 +2355,13 @@ "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." }, "blockedDomainsDesc": { - "message": "Automatisches Ausfüllen und andere zugehörige Funktionen werden für diese Webseiten nicht angeboten. Sie müssen die Seite aktualisieren, damit die Änderungen wirksam werden." + "message": "Automatisches Ausfüllen und andere zugehörige Funktionen werden für diese Webseiten nicht angeboten. Du musst die Seite neu laden, damit die Änderungen wirksam werden." }, - "autofillBlockedNotice": { - "message": "Das automatische Ausfüllen ist für diese Website gesperrt. Dieses Verhalten kann in den Einstellungen überprüft oder geändert werden." + "autofillBlockedNoticeV2": { + "message": "Automatisches Ausfüllen ist für diese Website gesperrt." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Dies in den Einstellungen ändern" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -2808,14 +2826,14 @@ "message": "Entschlüsselungsfehler" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden konnte den unten aufgelisteten Tresoreintrag bzw. die Tresoreinträge nicht entschlüsseln." + "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Kontaktiere unser Customer Success Team", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "um zusätzlichen Datenverlust zu vermeiden.", + "message": ", um zusätzlichen Datenverlust zu vermeiden.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Eine Benachrichtigung wurde an dein Gerät gesendet." }, + "notificationSentDevicePart1": { + "message": "Entsperre Bitwarden auf deinem Gerät oder mit der" + }, + "notificationSentDeviceAnchor": { + "message": "Web-App" + }, + "notificationSentDevicePart2": { + "message": "Stelle vor der Genehmigung sicher, dass die Fingerabdruck-Phrase mit der unten stehenden übereinstimmt." + }, "aNotificationWasSentToYourDevice": { "message": "Eine Benachrichtigung wurde an dein Gerät gesendet" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Stelle sicher, dass dein Konto entsperrt ist und die Fingerabdruck-Phrase mit der vom anderen Gerät übereinstimmt" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Du wirst benachrichtigt, sobald die Anfrage genehmigt wurde" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Anmeldung eingeleitet" }, + "logInRequestSent": { + "message": "Anfrage gesendet" + }, "exposedMasterPassword": { "message": "Kompromittiertes Master-Passwort" }, @@ -3425,38 +3452,6 @@ "message": "Ein-/ausklappen", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Deine Daten in Bitwarden importieren?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Deine LastPass-Daten schützen und in Bitwarden importieren?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Als unverschlüsselte Datei speichern", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "In Bitwarden importieren", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Wird importiert...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Daten erfolgreich importiert!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fehler beim Importieren. Überprüfe die Konsole für Details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netzwerkfehler beim Importieren.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias-Domain" }, @@ -3959,7 +3954,7 @@ "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Das Ignorieren dieser Option kann zu Konflikten zwischen dem Bitwarden Auto-Ausfüllen Menü und dem Browser führen.", + "message": "Das Ignorieren dieser Option kann zu Konflikten zwischen den Bitwarden Vorschlägen zum Auto-Ausfüllen und denen deines Browsers führen.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -4008,10 +4003,13 @@ "message": "Passkey entfernt" }, "autofillSuggestions": { - "message": "Vorschläge zum Auto-Ausfüllen" + "message": "Auto-Ausfüllen-Vorschläge" + }, + "itemSuggestions": { + "message": "Vorgeschlagene Einträge" }, "autofillSuggestionsTip": { - "message": "Speichere einen Login-Eintrag für diese Seite zum automatischen Ausfüllen" + "message": "Speichere einen Zugangsdaten-Eintrag für diese Seite zum automatischen Ausfüllen" }, "yourVaultIsEmpty": { "message": "Dein Tresor hat keine Einträge" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Eintrags-Name" }, - "cannotRemoveViewOnlyCollections": { - "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organisation ist deaktiviert" }, @@ -4586,17 +4575,11 @@ "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" + "message": "Anzahl der Auto-Ausfüllen-Vorschläge von Zugangsdaten auf dem Erweiterungssymbol anzeigen" }, "showQuickCopyActions": { "message": "Schnellkopier-Aktionen im Tresor anzeigen" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra breit" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Bitte aktualisiere deine Desktop-Anwendung" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Um biometrisches Entsperren zu verwenden, aktualisiere bitte deine Desktop-Anwendung oder deaktiviere die Entsperrung per Fingerabdruck in den Desktop-Einstellungen." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index bdb0eb2c17d2..7c9f6a7740e5 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Δημιουργία φράσης πρόσβασης" }, + "passwordGenerated": { + "message": "Ο κωδικός δημιουργήθηκε" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Επαναδημιουργία κωδικού πρόσβασης" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Επιβεβαίωση ταυτότητας" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Το vault σας είναι κλειδωμένο. Επαληθεύστε τον κύριο κωδικό πρόσβασης για να συνεχίσετε." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ζητήστε να προσθέσετε ένα αντικείμενο αν δε βρεθεί στο θησαυ/κιό σας. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, - "showCardsInVaultView": { - "message": "Εμφάνιση καρτών ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Εμφάνιση καρτών στη σελίδα Καρτέλας" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Εμφάνισε τα αντικείμενα κάρτες στη σελίδα Καρτέλα για εύκολη αυτόματη συμπλήρωση." }, - "showIdentitiesInVaultView": { - "message": "Εμφάνιση ταυτοτήτων ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Εμφάνιση ταυτοτήτων στη σελίδα καρτέλας" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Αγορά Premium έκδοσης" }, - "premiumPurchaseAlert": { - "message": "Μπορείτε να αγοράσετε συνδρομή Premium στο διαδικτυακό θησαυ/κιο του bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" - }, "premiumPurchaseAlertV2": { "message": "Μπορείτε να αγοράσετε το Premium από τις ρυθμίσεις του λογαριασμού σας στην διαδικτυακή εφαρμογή Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Γεννήτρια ονόματος χρήστη" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Χρήση αυτού του κωδικού πρόσβασης" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Η αυτόματη συμπλήρωση και άλλες σχετικές λειτουργίες δεν θα προσφερθούν για αυτούς τους ιστότοπους. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Ιστοσελίδα $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Μια ειδοποίηση στάλθηκε στη συσκευή σας" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Βεβαιωθείτε ότι ο λογαριασμός σας είναι ξεκλείδωτος και ότι η φράση δακτυλικού αποτυπώματος ταιριάζει στην άλλη συσκευή" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Θα ειδοποιηθείτε μόλις εγκριθεί η αίτηση" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Η σύνδεση ξεκίνησε" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Εκτεθειμένος Κύριος Κωδικός Πρόσβασης" }, @@ -3425,38 +3452,6 @@ "message": "Εναλλαγή σύμπτυξης", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Εισαγωγή των δεδομένων σας στο Bitwarden;", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Προστατέψτε τα δεδομένα LastPass και εισαγάγετε στο Bitwarden;", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Αποθήκευση ως μη κρυπτογραφημένο αρχείο", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Εισαγωγή στο Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Εισαγωγή...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Επιτυχής εισαγωγή δεδομένων!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Σφάλμα εισαγωγής. Ελέγξτε την κονσόλα για λεπτομέρειες.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Εμφανίστηκε σφάλμα δικτύου κατά την εισαγωγή.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Ψευδώνυμο τομέα" }, @@ -4008,7 +4003,10 @@ "message": "Το κλειδί πρόσβασης αφαιρέθηκε" }, "autofillSuggestions": { - "message": "Προτάσεις αυτόματης συμπλήρωσης" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Αποθηκεύστε ένα αντικείμενο σύνδεσης για την αυτόματη συμπλήρωση αυτού του ιστοτόπου" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Όνομα αντικειμένου" }, - "cannotRemoveViewOnlyCollections": { - "message": "Δεν μπορείτε να αφαιρέσετε συλλογές που έχουν μόνο δικαιώματα Προβολής: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Ο οργανισμός απενεργοποιήθηκε" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Send κειμένων" }, - "bitwardenNewLook": { - "message": "Το Bitwarden έχει μια νέα εμφάνιση!" - }, - "bitwardenNewLookDesc": { - "message": "Είναι πιο ευκολότερο και πιο διαισθητικό από ποτέ στην αυτόματη συμπλήρωση και αναζήτηση από την καρτέλα Θησαυ/κιο. Ρίξτε μια ματιά τριγύρω!" - }, "accountActions": { "message": "Ενέργειες λογαριασμού" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Εξαιρετικά φαρδύ" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Δεν μπορείτε να αφαιρέσετε συλλογές που έχουν μόνο δικαιώματα Προβολής: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 51e1203673b2..8698315b57c8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4160,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4900,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 7e938bb6b2ca..d591603c4208 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4008,7 +4003,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to auto-fill" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organisation is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 70e725c3a887..a5bccf2ae835 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identifies as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4008,7 +4003,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to auto-fill" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organisation is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index d9cd25178162..d440e1709281 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerar contraseña" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidad" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Mostrar las tarjetas en la pestaña" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Listar los elementos de tarjetas en la página para facilitar el auto-rellenado." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Mostrar las identidades en la página" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Comprar Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generador de nombres de usuario" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Usar esta contraseña" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Se ha enviado una notificación a tu dispositivo." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Inicio de sesión en proceso" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Contraseña maestra comprometida" }, @@ -3425,38 +3452,6 @@ "message": "Colapsar/Expandir", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "¿Quiere importar sus datos a Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "¿Quiere proteger sus datos de LastPass e importarlos a Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guardar como archivo no cifrado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar a Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importando...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Se importaron los datos correctamente.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Se produjo un error al importar. Revise la consola para obtener detalles.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Se produjo un error de red durante la importación.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Seudónimo del dominio" }, @@ -4008,7 +4003,10 @@ "message": "Clave de acceso eliminada" }, "autofillSuggestions": { - "message": "Autocompletar sugerencias" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Guarda un elemento de inicio de sesión para este sitio para autocompletar" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nombre del elemento" }, - "cannotRemoveViewOnlyCollections": { - "message": "No puedes eliminar colecciones con permisos de solo visualización: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "La organización está desactivada" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "No puedes eliminar colecciones con permisos de solo visualización: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 5245de4fb7ef..30be22a7d6e3 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Genereeri parool uuesti" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identiteedi kinnitamine" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Hoidla on lukus. Jätkamiseks sisesta ülemparool." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Kuva \"Kaart\" vaates kaardiandmed" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kuvab \"Kaart\" vaates kaardiandmeid, et neid saaks kiiresti sisestada" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Kuva \"Kaart\" vaates identiteete" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Osta Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Kasutajanime genereerija" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Kasuta seda parooli" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Sinu seadmesse saadeti teavitus." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Sisselogimine on käivitatud" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Ülemparool on haavatav" }, @@ -3425,38 +3452,6 @@ "message": "Peida", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Impordin andmed Bitwardenisse?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Kaitse oma LastPassi andmeid ja impordi need Bitwardenisse?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salvesta ilma krüpteeringuta failina", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Impordi Bitwardenisse", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importimine...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Andmed on edukalt imporditud!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Ilmnes viga. Vaata täpsemaid andmeid konsoolist.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Importimisel ilmnes võrgu viga.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domeen" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index fc875be6da06..4730c5918c6c 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Berrezarri pasahitza" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Zure identitatea egiaztatu" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Zure kutxa gotorra blokeatuta dago. Egiaztatu zure identitatea jarraitzeko." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Erakutsi txartelak fitxa orrian" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Erakutsi elementuen txartelak fitxa orrian, erraz auto-betetzeko." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Erakutsi identitateak fitxa orrian" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium erosi" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Inportatzen...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Datuak zuzen inportatu dira!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Errorea gertatu da inportatzean. Begiratu xehetasunak kontsolan.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Sareko errorea gertatu da inportatzerakoan.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index d2362a2655f5..8f414b35725a 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "تولید مجدد کلمه عبور" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "تأیید هویت" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "گاوصندوق شما قفل شده است. برای ادامه هویت خود را تأیید کنید." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "نمایش کارت‌ها در صفحه برگه" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "برای پر کردن خودکار آسان، موارد کارت را در صفحه برگه فهرست کن." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "نشان دادن هویت در صفحه برگه" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "خرید پرمیوم" }, - "premiumPurchaseAlert": { - "message": "شما می‌توانید عضویت پرمیوم را از گاوصندوق وب bitwarden.com خریداری کنید. مایلید اکنون از وب‌سایت بازید کنید؟" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "یک اعلان به دستگاه شما ارسال شده است." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "ورود به سیستم آغاز شد" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "کلمه عبور اصلی افشا شده" }, @@ -3425,38 +3452,6 @@ "message": "دکمه بستن", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "دامنه مستعار" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 0dd96ef9a026..e06f927072a6 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -120,7 +120,7 @@ "message": "Kopioi salasana" }, "copyPassphrase": { - "message": "Kopioi salalause" + "message": "Kopioi salauslauseke" }, "copyNote": { "message": "Kopioi merkinnät" @@ -443,7 +443,19 @@ "message": "Luo salasana" }, "generatePassphrase": { - "message": "Luo salalause" + "message": "Luo salauslauseke" + }, + "passwordGenerated": { + "message": "Salasana luotiin" + }, + "passphraseGenerated": { + "message": "Salauslauseke luotiin" + }, + "usernameGenerated": { + "message": "Käyttäjätunnus luotiin" + }, + "emailGenerated": { + "message": "Sähköpostiosoite luotu" }, "regeneratePassword": { "message": "Luo uusi salasana" @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Vahvista henkilöllisyytesi" }, + "weDontRecognizeThisDevice": { + "message": "Emme tunnista tätä laitetta. Anna sähköpostiisi lähetetty koodi henkilöllisyytesi vahvistamiseksi." + }, + "continueLoggingIn": { + "message": "Jatka kirjautumista" + }, "yourVaultIsLocked": { "message": "Holvisi on lukittu. Jatka vahvistamalla henkilöllisyytesi." }, @@ -986,8 +1004,8 @@ "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ä" + "showCardsInVaultViewV2": { + "message": "Näytä aina kortit automaattisen täytön ehdotuksina Holvi-näkymässä" }, "showCardsCurrentTab": { "message": "Näytä kortit välilehtiosiossa" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Näytä kortit Välilehti-sivulla automaattitäytön helpottamiseksi." }, - "showIdentitiesInVaultView": { - "message": "Näytä henkilöllisyydet automaattitäytön ehdotuksina Holvi-sivulla." + "showIdentitiesInVaultViewV2": { + "message": "Näytä aina identiteetit automaattisen täytön ehdotuksina Holvi-näkymässä" }, "showIdentitiesCurrentTab": { "message": "Näytä henkilöllisyydet välilehtiosiossa" @@ -1005,7 +1023,7 @@ "message": "Näytä henkilöllisyydet Välilehti-sivulla automaattitäytön helpottamiseksi." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Valitse kohteita täyttääksesi tiedot automaattisesti Holvi-näkymässä" }, "clearClipboard": { "message": "Tyhjennä leikepöytä", @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Osta Premium" }, - "premiumPurchaseAlert": { - "message": "Voit ostaa Premium-jäsenyyden bitwarden.com-verkkoholvista. Haluatko avata sivuston nyt?" - }, "premiumPurchaseAlertV2": { "message": "Voit ostaa Premiumin tiliasetuksistasi Bitwardenin verkkosovelluksen kautta." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Käyttäjätunnusgeneraattori" }, + "useThisEmail": { + "message": "Käytä tätä sähköpostia" + }, "useThisPassword": { "message": "Käytä tätä salasanaa" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Estetyt verkkotunnukset" }, "excludedDomains": { "message": "Ohitettavat verkkotunnukset" @@ -2337,13 +2355,13 @@ "message": "Bitwarden ei pyydä kirjautumistietojen tallennusta näillä verkkotunnuksilla. Koskee kaikkia kirjautuneita tilejä. Ota muutokset käyttöön päivittämällä sivu." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Näille sivustoille ei tarjota automaattista täyttöä eikä muita siihen liittyviä ominaisuuksia. Sinun on päivitettävä sivu, jotta muutokset tulevat voimaan." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Automaattitäyttö on estetty tällä sivustolla." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Muuta tätä asetuksissa" }, "websiteItemLabel": { "message": "Verkkotunnus $number$ (URI)", @@ -2364,7 +2382,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Estetyn verkkotunnuksen muutokset tallennettu" }, "excludedDomainsSavedSuccess": { "message": "Rajoitettujen verkkotunnusten muutokset tallennettiin" @@ -2805,17 +2823,17 @@ "message": "Virhe" }, "decryptionError": { - "message": "Decryption error" + "message": "Salauksen purkuvirhe" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden ei pystynyt purkamaan alla lueteltuja holvin kohteita." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Ota yhteyttä asiakkaaseen", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "lisätietojen menettämisen välttämiseksi.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -2849,7 +2867,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Käytä $RECOMMENDED$ tai useampaa sanaa vahvan salalauseen luomiseen.", + "message": " Käytä $RECOMMENDED$ tai useampaa sanaa vahvan salauslausekkeen luomiseen.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Laitteellesi on lähetetty ilmoitus." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Laitteeseesi lähetettiin ilmoitus" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Varmista, että vahvistavan laitteen holvi on avattu ja että se näyttää saman tunnistelausekkeen" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Sinulle ilmoitetaan, kun pyyntö on hyväksytty" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Kirjautuminen aloitettu" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Paljastunut pääsalasana" }, @@ -3425,38 +3452,6 @@ "message": "Laajenna tai supista", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Haluatko tuoda tietosi Bitwardeniin?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Haluatko suojata LastPass-tietosi tuomalla ne Bitwardeniin?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Tallenna salaamattomana tiedostona", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Tuo Bitwardeniin", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Tuodaan...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Tietojen tuonti onnistui.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Tuontivirhe. Näet isätietoja hallinnasta.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Verkkovirhe tuonnin aikana.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliaksen verkkotunnus" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Automaattitäytön ehdotukset" }, + "itemSuggestions": { + "message": "Ehdotetut kohteet" + }, "autofillSuggestionsTip": { "message": "Tallenna tälle sivustolle automaattisesti täytettävä kirjautumistieto." }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Kohteen nimi" }, - "cannotRemoveViewOnlyCollections": { - "message": "Et voi poistaa kokoelmia, joihin sinulla on vain tarkasteluoikeus: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organisaatio on poistettu käytöstä" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Teksti-Sendit" }, - "bitwardenNewLook": { - "message": "Bitwardenilla on uusi ulkoasu!" - }, - "bitwardenNewLookDesc": { - "message": "Automaattinen täyttö ja sisällön haku Holvi-välilehdeltä on nyt entistä helpompaa ja luontevampaa. Kokeile nyt!" - }, "accountActions": { "message": "Tilitoiminnot" }, @@ -4671,22 +4654,22 @@ "message": "Sinulla ei ole oikeutta muokata tätä kohdetta" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Biometrinen avaus ei ole käytettävissä, koska PIN-koodi tai salasanan lukituksen avaus vaaditaan ensin." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Biometrinen avaus ei tällä hetkellä ole käytettävissä." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biometrinen avaus ei ole käytettävissä, koska järjestelmätiedostoja ei ole määritetty." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biometrinen avaus ei ole käytettävissä, koska järjestelmätiedostoja ei ole määritetty." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Biometrinen avaus ei ole käytettävissä, koska Bitwardenin työpöytäsovellus on suljettu." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Biometrinen avaus ei ole käytettävissä, koska sitä ei ole otettu käyttöön osoitteelle $EMAIL$ Bitwardenin työpöytäsovelluksessa.", "placeholders": { "email": { "content": "$1", @@ -4695,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Biometrinen avaus ei ole tällä hetkellä käytettävissä tuntemattomasta syystä." }, "authenticating": { "message": "Todennetaan" @@ -4869,13 +4852,13 @@ "message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Voit ottaa käyttöön kaksivaiheisen kirjautumisen vaihtoehtoisena tapana suojata tilisi, tai vaihtaa sähköpostisi sellaiseen, johon sinulla on pääsy." }, "remindMeLater": { "message": "Muistuta myöhemmin" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Onko sinulla luotettava pääsy sähköpostiisi, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4884,10 +4867,10 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Ei ole" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Kyllä on" }, "turnOnTwoStepLogin": { "message": "Ota kaksivaiheinen kirjautuminen käyttöön" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Erittäin leveä" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Et voi poistaa kokoelmia, joihin sinulla on vain tarkasteluoikeus: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Päivitä työpöytäsovellus" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Käyttääksesi biometristä avausta, päivitä työpöytäsovelluksesi tai poista tunnistelauseke käytöstä työpöydän asetuksista." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index d0fdf1018fb5..d5f1ee430a52 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Muling I-generate ang Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "I-verify ang pagkakakilanlan" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Naka-lock ang iyong vault. Patunayan ang iyong pagkakakilanlan upang magpatuloy." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Ipakita ang mga card sa Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Itala ang mga item ng card sa Tab page para sa madaling auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Ipakita ang mga pagkatao sa Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Bilhin ang Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Naipadala na ang notification sa iyong device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Nakalantad na Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 913391d218cd..a52cb300532b 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Générer une phrase de passe" }, + "passwordGenerated": { + "message": "Mot de passe généré" + }, + "passphraseGenerated": { + "message": "Phrase de passe générée" + }, + "usernameGenerated": { + "message": "Nom d'utilisateur généré" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Régénérer un mot de passe" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Vérifier l'identité" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continuer à se connecter" + }, "yourVaultIsLocked": { "message": "Votre coffre est verrouillé. Vérifiez votre identité pour continuer." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Afficher les cartes de paiement sur la Page d'onglet" @@ -995,8 +1013,8 @@ "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" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Afficher les identités sur la Page d'onglet" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Acheter Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Générateur de nom d'utilisateur" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Utiliser ce mot de passe" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Site web $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Une notification a été envoyée à votre appareil." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Une notification a été envoyée à votre appareil" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Assurez-vous que votre compte est déverrouillé et que la phrase d'empreinte correspond à l'autre appareil" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Vous serez notifié une fois que la demande sera approuvée" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Connexion initiée" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Mot de passe principal exposé" }, @@ -3425,38 +3452,6 @@ "message": "Déplier/Replier", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importer vos données dans Bitwarden ?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protéger vos données LastPass et importer dans Bitwarden ?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Enregistrer en tant que fichier non chiffré", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importer vers Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importation en cours...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Données importées avec succès !", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erreur lors de l'importation. Consultez la console pour plus de détails.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Une erreur réseau s'est produite lors de l'importation.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domaine de l'alias" }, @@ -4008,7 +4003,10 @@ "message": "Clé d'identification (passkey) retirée" }, "autofillSuggestions": { - "message": "Suggestions de saisie automatique" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Enregistrez un élément de connexion à remplir automatiquement pour ce site" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nom de l’élément" }, - "cannotRemoveViewOnlyCollections": { - "message": "Vous ne pouvez pas supprimer des collections avec les autorisations d'affichage uniquement : $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "L'organisation est désactivée" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Très large" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Vous ne pouvez pas supprimer des collections avec les autorisations d'affichage uniquement : $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index e655159f2460..e92bf5c06b88 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -382,7 +382,7 @@ "message": "Aniñar un cartafol engadindo o nome do cartafol pai seguido dun \"/\". Exemplo: Social/Foros" }, "noFoldersAdded": { - "message": "Sen cartafois" + "message": "Sen cartafoles" }, "createFoldersToOrganize": { "message": "Crea cartafoles para organizar as entradas da túa caixa forte" @@ -394,10 +394,10 @@ "message": "Eliminar cartafol" }, "folders": { - "message": "Cartafois" + "message": "Cartafoles" }, "noFolders": { - "message": "Non hai cartafois que listar." + "message": "Non hai cartafoles que listar." }, "helpFeedback": { "message": "Axuda e comentarios" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Xerar frase de contrasinal" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Volver xerar contrasinal" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "A túa caixa forte está bloqueada. Verifica a túa identidade para continuar." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ofrecer gardar un elemento se non se atopa na caixa forte. Aplica a tódalas sesións iniciadas." }, - "showCardsInVaultView": { - "message": "Na caixa forte, amosar tarxetas como suxestións de Autoenchido" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Amosar tarxetas na pestana" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Lista na pestana actual as tarxetas gardadas para autoenchido." }, - "showIdentitiesInVaultView": { - "message": "Na caixa forte, amosar identidades como suxestións de Autoenchido" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Amosar identidades na pestana" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Adquirir Prémium" }, - "premiumPurchaseAlert": { - "message": "Podes adquirir o plan Prémium na aplicación web de bitwarden.com. Queres visitala agora mesmo?" - }, "premiumPurchaseAlertV2": { "message": "Podes adquirir o plan Prémium dende os axustes de conta da aplicación web de Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Xerador de nomes de usuario" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Usar este contrasinal" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Dominios bloqueados" }, "excludedDomains": { "message": "Dominios excluídos" @@ -2337,13 +2355,13 @@ "message": "Bitwarden non ofrecerá gardar contas para estes dominios en ningunha das sesións iniciadas. Recarga a páxina para que os cambios fornezan efecto." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "O autoenchido e outras funcións relacionadas non estarán dispoñibles para estas webs. Debes recargar a páxina para que os cambios teñan efecto." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "O autoenchido está bloqueado para esta web." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Cambia isto en axustes" }, "websiteItemLabel": { "message": "Web $number$ (URI)", @@ -2364,7 +2382,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Dominios bloqueados gardados" }, "excludedDomainsSavedSuccess": { "message": "Dominios excluídos gardados" @@ -2805,17 +2823,17 @@ "message": "Erro" }, "decryptionError": { - "message": "Decryption error" + "message": "Erro de descifrado" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden non puido descifrar os seguintes elementos." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Contacto co cliente exitoso", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "para evitar a perda de datos.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Enviouse unha notificación ó teu dispositivo." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Enviouse unha notificación ó teu dispositivo" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Por favor asegúrate de que a sesión está aberta e a frase de pegada dixital coincide ca do outro dispositivo" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Serás notificado unha vez se aprobe a solicitude" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Inicio de sesión comezado" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Contrasinal mestre filtrado" }, @@ -3425,38 +3452,6 @@ "message": "Colapsar/Expandir", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importar os teus datos a Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protexer os teus datos de LastPass e importar a Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Gardar como arquivo sen cifrar", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar a Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importando...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Datos importados con éxito!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erro importando. Comproba a consola para máis detalle.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Aconteceu un erro de rede durante a importación.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias do dominio" }, @@ -3824,7 +3819,7 @@ "message": "Autenticación multifactor fallida" }, "includeSharedFolders": { - "message": "Incluír cartafois compartidos" + "message": "Incluír cartafoles compartidos" }, "lastPassEmail": { "message": "Correo de LastPass" @@ -4008,7 +4003,10 @@ "message": "Clave de acceso eliminada" }, "autofillSuggestions": { - "message": "Suxestións de autoenchido" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Entradas suxeridas" }, "autofillSuggestionsTip": { "message": "Gardar unha credencial como suxestión para este sitio" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nome da entrada" }, - "cannotRemoveViewOnlyCollections": { - "message": "Non podes eliminar coleccións con permisos de Só lectura: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "A organización está desactivada" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Textos Send" }, - "bitwardenNewLook": { - "message": "Bitwarden ten un novo look!" - }, - "bitwardenNewLookDesc": { - "message": "É máis fácil e intuitivo que nunca autoencher e buscar dende a caixa forte. Bota un ollo!" - }, "accountActions": { "message": "Accións da conta" }, @@ -4671,22 +4654,22 @@ "message": "Non tes permiso para modificar esta entrada" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "O desbloqueo biométrico non está dispoñible porque se require o PIN ou contrasinal primeiro." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "O desbloqueo biométrico non está dispoñible." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "O desbloqueo biométrico non está dispoñible por arquivos do sistema desconfigurados." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "O desbloqueo biométrico non está dispoñible por arquivos do sistema desconfigurados." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "O desbloqueo biométrico non está dispoñible porque a aplicación de escritorio está pechada." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "O desbloqueo biométrico non está dispoñible porque non está activada para $EMAIL$ na aplicación de escritorio.", "placeholders": { "email": { "content": "$1", @@ -4695,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "O desbloqueo biométrico non está dispoñible por algunha razón non prevista." }, "authenticating": { "message": "Autenticando" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Moi ancho" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Non podes eliminar coleccións con permisos de Só lectura: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 1a3057ac2913..a5e126046a76 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "צור סיסמה חדשה" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "אימות זהות" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "הכספת שלך נעולה. הזן את הסיסמה הראשית שלך כדי להמשיך." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "רכוש פרימיום" }, - "premiumPurchaseAlert": { - "message": "באפשרותך לרכוש מנוי פרימיום בכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4008,7 +4003,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "הצעות למילוי אוטומטי" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index fd4a6612af41..e3a1b690bf35 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "पहचान सत्यापित करें" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "आपकी वॉल्ट लॉक हो गई है। जारी रखने के लिए अपने मास्टर पासवर्ड को सत्यापित करें।" }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "टैब पेज पर कार्ड दिखाएं" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "आसान ऑटो-फिल के लिए टैब पेज पर कार्ड आइटम सूचीबद्ध करें।" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "टैब पेज पर पहचान दिखाएं" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "आप bitwarden.com वेब वॉल्ट पर प्रीमियम सदस्यता खरीद सकते हैं।क्या आप अब वेबसाइट पर जाना चाहते हैं?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "डोमेन उपनाम" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "बिटवार्डन का नया रूप!" - }, - "bitwardenNewLookDesc": { - "message": "वॉल्ट टैब से ऑटोफिल और सर्च करना पहले से कहीं ज़्यादा आसान और सहज है। सबकुछ ध्यान से देखें!" - }, "accountActions": { "message": "Account actions" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 331bc109309a..b604d562181e 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generiraj frazu lozinke" }, + "passwordGenerated": { + "message": "Lozinka generirana" + }, + "passphraseGenerated": { + "message": "Frazna lozinka generirana" + }, + "usernameGenerated": { + "message": "Korisničko ime generirano" + }, + "emailGenerated": { + "message": "e-pošta generirana" + }, "regeneratePassword": { "message": "Ponovno generiraj lozinku" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Potvrdi identitet" }, + "weDontRecognizeThisDevice": { + "message": "Ne prepoznajemo ovaj uređaj. Za potvrdu identiteta unesi kôd poslan e-poštom." + }, + "continueLoggingIn": { + "message": "Nastavi prijavu" + }, "yourVaultIsLocked": { "message": "Tvoj trezor je zaključan. Potvrdi glavnu lozinku za nastavak." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Uvijek prikaži kartice kao prijedloge za auto-ispunu u prikazu trezora" }, "showCardsCurrentTab": { "message": "Prikaži platne kartice" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Prikazuj platne kartice za jednostavnu auto-ispunu." }, - "showIdentitiesInVaultView": { - "message": "Prikaži identitete kao prijedloge za auto-ispunu u prikazu trezora" + "showIdentitiesInVaultViewV2": { + "message": "Uvijek prikaži identitete kao prijedloge za auto-ispunu u prikazu trezora" }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Kupi premium članstvo" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generator korisničkih imena" }, + "useThisEmail": { + "message": "Koristi ovu e-poštu" + }, "useThisPassword": { "message": "Koristi ovu lozinku" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Blokirane domene" }, "excludedDomains": { "message": "Izuzete domene" @@ -2337,13 +2355,13 @@ "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." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Auto-ispuna i druge vezane značajke neće biti ponuđene za ova web mjesta. Potrebno je osvježiti stranicu zaprimjenu postavki." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Auto-ispuna je blokirana za ovu web stranicu." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Promijeni ovo u postavkama" }, "websiteItemLabel": { "message": "Web stranica $number$ (URI)", @@ -2364,7 +2382,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Spremljene promjene blokiranih domena" }, "excludedDomainsSavedSuccess": { "message": "Spremljene promjene izuzete domene" @@ -2805,17 +2823,17 @@ "message": "Pogreška" }, "decryptionError": { - "message": "Decryption error" + "message": "Pogreška pri dešifriranju" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden nije mogao dešifrirati sljedeće stavke trezora." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Kontaktiraj službu za korisnike", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "kako bi izbjegli gubitak podataka.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Obavijest je poslana na tvoj uređaj." }, + "notificationSentDevicePart1": { + "message": "Otključaj Bitwarden na svojem uređaju ili na" + }, + "notificationSentDeviceAnchor": { + "message": "web trezoru" + }, + "notificationSentDevicePart2": { + "message": "Provjeri slaže li se jedinstvena fraza s ovdje prikazanom prije odobravanja." + }, "aNotificationWasSentToYourDevice": { "message": "Obavijest je poslana na tvoj uređaj" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Provjeri je li trezor otključan i slaže li se jedinstvena fraza s drugim uređajem" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Dobiti ćeš obavijest kada je tvoj zahtjev odobren" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Prijava pokrenuta" }, + "logInRequestSent": { + "message": "Zahtjev poslan" + }, "exposedMasterPassword": { "message": "Ukradena glavna lozinka" }, @@ -3425,38 +3452,6 @@ "message": "Sažmi/Proširi", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Uvezi svoje podatke u Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Zaštititi svoje LastPass podatke i uvezi ih u Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Spremi kao nekriptiranu datoteku", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Uvezi u Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Uvoz...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Uvoz podataka u trezor uspješan!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Greška pri uvozu. Provjeri konzolu za detalje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Došlo je do mrežne greške tijekom uvoza.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domene" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Prijedlozi auto-ispune" }, + "itemSuggestions": { + "message": "Predložene stavke" + }, "autofillSuggestionsTip": { "message": "Spremi u auto-ispunu stavku prijave za ovu stranicu" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Naziv stavke" }, - "cannotRemoveViewOnlyCollections": { - "message": "S dopuštenjima samo za prikaz ne možeš ukloniti zbirke: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organizacija je deaktivirana" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4671,22 +4654,22 @@ "message": "Nemaš prava za uređivanje ove stavke" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Biometrijsko otključavanje nije dostupno jer je prvo potrebno otključati PIN-om ili lozinkom." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Biometrijsko otključavanje trenutno nije dostupno." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biometrijsko otključavanje nije dostupno zbog pogrešno konfiguriranih sistemskih datoteka." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biometrijsko otključavanje nije dostupno zbog pogrešno konfiguriranih sistemskih datoteka." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Biometrijsko otključavanje nije dostupno jer je Bitwarden dekstop aplikacija zatvorena." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Biometrijsko otključavanje nije dostupno jer nije omogućeno za $EMAIL$ u Bitwarden desktop aplikaciji.", "placeholders": { "email": { "content": "$1", @@ -4695,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Biometrijsko otključavanje trenutno nije dostupno iz nepoznatog razloga." }, "authenticating": { "message": "Autentifikacija" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Ekstra široko" + }, + "cannotRemoveViewOnlyCollections": { + "message": "S dopuštenjima samo za prikaz ne možeš ukloniti zbirke: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Molimo, ažuriraj svoju desktop aplikaciju" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Za korištenje biometrijskog otključavanja ažuriraj desktop aplikaciju ili nemogući otključavanje otiskom prsta u desktop aplikaciji." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 0aea2c7ecedf..14f6431b4cb7 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Jelmondat generálás" }, + "passwordGenerated": { + "message": "A jelszó generálásra került." + }, + "passphraseGenerated": { + "message": "A jelmondat generálásra került." + }, + "usernameGenerated": { + "message": "A felhasználónév generálásra került." + }, + "emailGenerated": { + "message": "Az email generálásra került." + }, "regeneratePassword": { "message": "Jelszó újragenerálása" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Személyazonosság ellenőrzése" }, + "weDontRecognizeThisDevice": { + "message": "Nem ismerhető fel ez az eszköz. Írjuk be az email címünkre küldött kódot a személyazonosság igazolásához." + }, + "continueLoggingIn": { + "message": "A bejelentkezés folytatása" + }, "yourVaultIsLocked": { "message": "A széf zárolásra került. A folytatáshoz meg kell adni a mesterjelszót." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Mindig jelenítse meg a kártyákat automatikus kitöltési javaslatként a Széf nézetben" }, "showCardsCurrentTab": { "message": "Kártyák megjelenítése a Fül oldalon" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kártyaelemek listázása a Fül oldalon a könnyű automatikus kitöltéshez." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Mindig jelenítse meg a személyazonosságokat automatikus kitöltési javaslatként a Széf nézetben" }, "showIdentitiesCurrentTab": { "message": "Azonosítások megjelenítése a Fül oldalon" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Prémium funkció megvásárlása" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Felhasználónév generátor" }, + "useThisEmail": { + "message": "Ezen email használata" + }, "useThisPassword": { "message": "Jelszó használata" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Az automatikus kitöltés és az egyéb kapcsolódó funkciók ezeken a webhelyeken nincsenek a kínálatban. A változtatások életbe lépéséhez frissíteni kell az oldalt." }, - "autofillBlockedNotice": { - "message": "Az automatikus kitöltés le van tiltva ezen a webhelyen. Tekintsük át vagy módosítsuk ezt a beállításokban." + "autofillBlockedNoticeV2": { + "message": "Az automatikus kitöltés blokkolásra került ezen a webhelyen." }, - "autofillBlockedTooltip": { - "message": "Az automatikus kitöltés le van tiltva ezen a webhelyen. Tekintsük át ezt a beállításokban." + "autofillBlockedNoticeGuidance": { + "message": "Megváltoztatás a beállításokban" }, "websiteItemLabel": { "message": "Webhely $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Egy értesítés lett elküldve az eszközre." }, + "notificationSentDevicePart1": { + "message": "A Bitwarden zárolás feloldása az eszközön vagy: " + }, + "notificationSentDeviceAnchor": { + "message": "webalkalmazás" + }, + "notificationSentDevicePart2": { + "message": "Jóváhagyás előtt győződjünk meg arról, hogy az ujjlenyomat kifejezés megegyezik az alábbi kifejezéssel." + }, "aNotificationWasSentToYourDevice": { "message": "Egy értesítés lett elküldve az eszközre." }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Ellenőrizzük, hogy a széf feloldásra került és az ujjlenyomat kifejezés egyezik a másik eszközön levővel." - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "A kérelem jóváhagyása után értesítés érkezik." }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "A bejelentkezés elindításra került." }, + "logInRequestSent": { + "message": "A kérés elküldésre került." + }, "exposedMasterPassword": { "message": "Kiszivárgott mesterjelszó" }, @@ -3425,38 +3452,6 @@ "message": "Összezárás váltás", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Adatok importálása a Bitwardenbe?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "A LastPass adatok megvédése és importálása a Bitwardenbe?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Mentés titkosítatlan fájlként", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importálás a Bitwardenbe", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importálás...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Az adatok sikeresen importálásra kerültek.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Hiba történt az importálás során. A részletekért ellenőrizzük a konzolt.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Hálózati hiba történt az importálás során.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Áldomain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Automatikus kitöltés javaslatok" }, + "itemSuggestions": { + "message": "Javasolt elemek" + }, "autofillSuggestionsTip": { "message": "A bejelentkezési elem mentése ehhez a webhelyhez az automatikus kitöltéshez" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Elem neve" }, - "cannotRemoveViewOnlyCollections": { - "message": "Nem távolíthatók el a csak megtekintési engedéllyel bíró gyűjtemények: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra széles" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Nem távolíthatók el a csak megtekintési engedéllyel bíró gyűjtemények: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Frissítsük az asztali alkalmazást." + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "A biometrikus feloldás használatához frissítsük az asztali alkalmazást vagy tiltsuk le az ujjlenyomatos feloldást az asztali beállításokban." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 82776a8e82bb..53810e85f77d 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Buat frasa sandi" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Buat Ulang Kata Sandi" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifikasi Identitas Anda" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Brankas Anda terkunci. Verifikasi kata sandi utama Anda untuk melanjutkan." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Tanyakan untuk menambah sebuah benda jika benda itu tidak ditemukan di brankas Anda. Diterapkan ke seluruh akun yang telah masuk." }, - "showCardsInVaultView": { - "message": "Tampilkan kartu sebagai saran isi otomatis pada tampilan Brankas" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Tamplikan kartu pada halaman Tab" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Buat tampilan daftar benda dari kartu pada halaman Tab untuk isi otomatis yang mudah." }, - "showIdentitiesInVaultView": { - "message": "Tampilkan identitas sebagai saran isi otomatis pada tampilan Brankas" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Tampilkan identitas pada halaman Tab" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Beli Keanggotaan Premium" }, - "premiumPurchaseAlert": { - "message": "Anda dapat membeli keanggotaan premium di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" - }, "premiumPurchaseAlertV2": { "message": "Anda dapat membeli Premium dari pilihan akun Anda pada aplikasi web Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Pembuat nama pengguna" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Gunakan kata sandi ini" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Situs web $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Sebuah pemberitahuan dikirim ke perangkat Anda." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Sebuah pemberitahuan telah dikirim ke perangkat Anda" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Pastikan akun Anda terbuka dan frasa sidik jari cocok pada perangkat lainnya" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Anda akan diberitahu setelah permintaan disetujui" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Memulai login" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Kata Sandi Utama yang Terpapar" }, @@ -3425,38 +3452,6 @@ "message": "Saklar lipat", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Impor data Anda ke Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Lindungi data LastPass Anda dan impor ke Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Simpan sebagai berkas yang tidak dienkripsi", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Impor ke Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Mengimpor...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data berhasil diimpor!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Gagal mengimpor. Periksa konsol untuk rinciannya.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Kesalahan jaringan ditemui ketika mengimpor.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domain alias" }, @@ -4008,7 +4003,10 @@ "message": "Kunci sandi dihapus" }, "autofillSuggestions": { - "message": "Saran isi otomatis" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Simpan benda login untuk situs ini ke isi otomatis" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nama benda" }, - "cannotRemoveViewOnlyCollections": { - "message": "Anda tidak dapat menghapus koleksi dengan izin hanya lihat: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organisasi dinonaktifkan" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Ekstra lebar" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Anda tidak dapat menghapus koleksi dengan izin hanya lihat: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 04e2c4ee64f0..9c00396b4505 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Genera passphrase" }, + "passwordGenerated": { + "message": "Parola d'accesso generata" + }, + "passphraseGenerated": { + "message": "Frase d'accesso generata" + }, + "usernameGenerated": { + "message": "Nome utente generato" + }, + "emailGenerated": { + "message": "E-mail generata" + }, "regeneratePassword": { "message": "Rigenera password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifica identità" }, + "weDontRecognizeThisDevice": { + "message": "Non riconosciamo questo dispositivo. Inserisci il codice inviato alla tua e-mail per verificare la tua identità." + }, + "continueLoggingIn": { + "message": "Continua l'accesso" + }, "yourVaultIsLocked": { "message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Mostra sempre le carte come suggerimenti di riempimento automatico nella vista cassaforte" }, "showCardsCurrentTab": { "message": "Mostra le carte nella sezione Scheda" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Mostra le carte nella sezione Scheda per riempirle automaticamente." }, - "showIdentitiesInVaultView": { - "message": "Mostra le identità come suggerimenti di riempimento automatico nella vista cassaforte" + "showIdentitiesInVaultViewV2": { + "message": "Mostra sempre le identità come suggerimenti di riempimento automatico nella vista cassaforte" }, "showIdentitiesCurrentTab": { "message": "Mostra le identità nella sezione Scheda" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Passa a Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generatore di nomi utente" }, + "useThisEmail": { + "message": "Usa questa e-mail" + }, "useThisPassword": { "message": "Usa questa password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Per questi siti, l'auto-completamento e funzionalità simili non saranno disponibili. Ricarica la pagina per applicare le modifiche." }, - "autofillBlockedNotice": { - "message": "L'auto-completamento è bloccato per questo sito. Modifica questa scelta nelle impostazioni." + "autofillBlockedNoticeV2": { + "message": "La compilazione automatica è bloccata per questo sito." }, - "autofillBlockedTooltip": { - "message": "L'auto-completamento è bloccato per questo sito. Verifica nelle impostazioni." + "autofillBlockedNoticeGuidance": { + "message": "Modifica questo nelle impostazioni" }, "websiteItemLabel": { "message": "Sito $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Una notifica è stata inviata al tuo dispositivo." }, + "notificationSentDevicePart1": { + "message": "Sblocca Bitwarden sul tuo dispositivo o su" + }, + "notificationSentDeviceAnchor": { + "message": "app web" + }, + "notificationSentDevicePart2": { + "message": "Assicurarsi che la frase di impronta digitale corrisponda a quella sottostante prima dell'approvazione." + }, "aNotificationWasSentToYourDevice": { "message": "Una notifica è stata inviata al tuo dispositivo" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Sarai notificato una volta che la richiesta sarà approvata" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Accesso avviato" }, + "logInRequestSent": { + "message": "Richiesta inviata" + }, "exposedMasterPassword": { "message": "Password principale violata" }, @@ -3425,38 +3452,6 @@ "message": "Comprimi/espandi", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importare i tuoi dati su Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteggere i tuoi dati LastPass e importarli su Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salva come file non crittografato", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importa su Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importazione in corso...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dati importati!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Errore durante l'importazione. Controlla la console per i dettagli.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Errore di connessione durante l'importazione.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Dominio alias" }, @@ -4008,7 +4003,10 @@ "message": "Passkey rimossa" }, "autofillSuggestions": { - "message": "Suggerimenti per il riempimento automatico" + "message": "Suggerimenti riempimento automatico" + }, + "itemSuggestions": { + "message": "Elementi suggeriti" }, "autofillSuggestionsTip": { "message": "Salva un elemento di accesso per questo sito da riempire automaticamente" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nome elemento" }, - "cannotRemoveViewOnlyCollections": { - "message": "Non puoi rimuovere raccolte con i soli permessi di visualizzazione: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "L'organizzazione è disattivata" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Molto larga" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Non puoi rimuovere raccolte con i soli permessi di visualizzazione: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aggiornare l'applicazione desktop" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Per usare lo sblocco biometrico, aggiornare l'applicazione desktop o disabilitare lo sblocco dell'impronta digitale nelle impostazioni del desktop." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index cc1f34e49856..ca8141cb59fd 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "パスフレーズを生成" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "パスワードの再生成" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "本人確認を行う" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "保管庫がロックされています。続行するには本人確認を行ってください。" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "保管庫にアイテムが見つからない場合は、アイテムを追加するよう要求します。ログインしているすべてのアカウントに適用されます。" }, - "showCardsInVaultView": { - "message": "保管庫ビューに自動入力の候補としてカードを表示する" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "タブページにカードを表示" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "自動入力を簡単にするために、タブページにカードアイテムを表示します" }, - "showIdentitiesInVaultView": { - "message": "保管庫ビューに自動入力の候補として ID を表示する" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "タブページに ID を表示" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "プレミアム会員に加入" }, - "premiumPurchaseAlert": { - "message": "プレミアム会員権は bitwarden.com ウェブ保管庫で購入できます。ウェブサイトを開きますか?" - }, "premiumPurchaseAlertV2": { "message": "Bitwarden ウェブアプリでアカウント設定からプレミアムを購入できます。" }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "ユーザー名生成ツール" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "このパスワードを使用する" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "ウェブサイト $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "デバイスに通知を送信しました。" }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "お使いのデバイスに通知が送信されました" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "アカウントがロック解除されていることと、フィンガープリントフレーズが他の端末で一致していることを確認してください" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "リクエストが承認されると通知されます" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "ログイン開始" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "流出したマスターパスワード" }, @@ -3425,38 +3452,6 @@ "message": "開く/閉じる", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Bitwarden にデータをインポートしますか?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass データを Bitwarden にインポートしますか?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "暗号化されていないファイルとして保存", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden にインポート", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "インポート中...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "データをインポートしました!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "インポート中にエラーが発生しました。詳細はコンソールを確認してください。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "インポート中にネットワークエラーが発生しました。", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "エイリアスドメイン" }, @@ -4008,7 +4003,10 @@ "message": "パスキーを削除しました" }, "autofillSuggestions": { - "message": "候補を自動入力する" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "自動入力するためにこのサイトのログインアイテムを保存します" @@ -4157,15 +4155,6 @@ "itemName": { "message": "アイテム名" }, - "cannotRemoveViewOnlyCollections": { - "message": "表示のみの権限が与えられているコレクションを削除することはできません: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "組織は無効化されています" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "テキスト Send" }, - "bitwardenNewLook": { - "message": "Bitwarden が新しい外観になりました。" - }, - "bitwardenNewLookDesc": { - "message": "保管庫タブからの自動入力と検索がこれまで以上に簡単で直感的になりました。" - }, "accountActions": { "message": "アカウントの操作" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "エクストラワイド" + }, + "cannotRemoveViewOnlyCollections": { + "message": "表示のみの権限が与えられているコレクションを削除することはできません: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 50fdc6613c50..5b81c015383e 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "შემოტანა...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "ჩანაწერის სახელი" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index e34751eea7dd..800523bdc2ec 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 3f9e99e56371..90359b0f73ba 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಪುನರುತ್ಪಾದಿಸಿ" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಲಾಕ್ ಆಗಿದೆ. ಮುಂದುವರೆಯಲು ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "ಪ್ರೀಮಿಯಂ ಖರೀದಿಸಿ" }, - "premiumPurchaseAlert": { - "message": "ನೀವು ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಪ್ರೀಮಿಯಂ ಸದಸ್ಯತ್ವವನ್ನು ಖರೀದಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 4ac6d281b094..896e22d57bbf 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "암호 생성" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "비밀번호 재생성" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "신원 확인" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "보관함이 잠겨 있습니다. 마스터 비밀번호를 입력하여 계속하세요." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, - "showCardsInVaultView": { - "message": "보관함 보기에서 카드 자동완성 제안를 표시" + "showCardsInVaultViewV2": { + "message": "보관함 보기에서 언제나 카드 자동 완성 제안을 표시" }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열" }, - "showIdentitiesInVaultView": { - "message": "보관함 보기에서 신원들의 자동완성 제안을 표시" + "showIdentitiesInVaultViewV2": { + "message": "보관함 보기에서 언제나 신원의 자동 완성 제안을 표시" }, "showIdentitiesCurrentTab": { "message": "탭 페이지에 신원들을 표시" @@ -1005,7 +1023,7 @@ "message": "간편한 자동완성을 위해 탭에 신원 항목들을 나열" }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "보관함 보기에서 항목을 클릭하여 자동 완성" }, "clearClipboard": { "message": "클립보드 비우기", @@ -1107,10 +1125,10 @@ "message": "이 비밀번호는 이 파일을 파일 내보내거나, 가져오는데 사용됩니다." }, "accountRestrictedOptionDescription": { - "message": "계정의 사용자 이름과 마스터 비밀번호에서 파생된 계정 암호화 키를 사용하여 내보내기를 암호화하고, 현재 Bitwarden계정으로 가져오기를 제한해보세요. " + "message": "내보내기를 당신의 계정의 사용자이름과 마스터비밀번호로부터 파생된 계정 암호화 키를 사용하여 암호화하고, 현재의 Bitwarden 계정으로만 가져오도록 제한합니다." }, "passwordProtectedOptionDescription": { - "message": "파일 비밀번호를 설정하여, 내보내기를 암호화하고, 해독에 그 파일 비밀번호를 사용하는 Bitwarden계정에 가져오세요." + "message": "파일에 비밀번호를 설정하여 내보내기를 암호화하고, 어느 Bitwarden 계정으로든 그 비밀번호로 해독하여 가져오기 합니다." }, "exportTypeHeading": { "message": "내보내기 유형" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "프리미엄 멤버십 구입" }, - "premiumPurchaseAlert": { - "message": "bitwarden.com 웹 보관함에서 프리미엄 멤버십을 구입할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" - }, "premiumPurchaseAlertV2": { "message": "Bitwarden 웹 앱의 계정 설정에서 프리미엄에 대한 결제를 할 수 있습니다." }, @@ -1478,7 +1493,7 @@ "message": "카드를 제안으로 표시" }, "showInlineMenuOnIconSelectionLabel": { - "message": "아이콘을 선택하면 제안이 표시됩니다." + "message": "아이콘을 선택할 때 제안을 표시" }, "showInlineMenuOnFormFieldsDescAlt": { "message": "로그인한 모든 계정에 적용" @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "사용자 이름 생성기" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "이 비밀번호 사용" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "웹사이트 $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "기기에 알림이 전송되었습니다." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "기기에 알림이 전송되었습니다." }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "반드시 계정이 잠금 해제되었고, 지문 구절이 다른 기기에서 일치하는지 확인해주세요." - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "요청이 승인되면 알림을 받게 됩니다" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "로그인 시작" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "노출된 마스터 비밀번호" }, @@ -3425,38 +3452,6 @@ "message": "토글이 붕괴됨", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "데이터를 Bitwarden으로 가져오시겠습니까?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass 데이터를 보호하고 Bitwarden으로 가져오시겠습니까?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "암호화되지 않은 파일로 저장", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden으로 가져오기", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "가져오는 중...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "데이터 가져오기 성공!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "가져오는 중 오류가 발생했습니다. 자세한 내용은 콘솔을 확인하세요.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "가져오기 중에 네트워크 오류가 발생했습니다.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "도메인 별칭" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "자동 완성 제안" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "이 사이트에서 자동으로 작성할 로그인 항목 저장" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "항목 이름" }, - "cannotRemoveViewOnlyCollections": { - "message": "보기 권한만 있는 컬렉션은 제거할 수 없습니다: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "조직이 비활성화되었습니다" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "텍스트 Send" }, - "bitwardenNewLook": { - "message": "Bitwarden이 새로운 모습으로 돌아왔습니다!" - }, - "bitwardenNewLookDesc": { - "message": "보관함 탭에서 자동 완성하고 검색하는 것이 그 어느 때보다 쉽고 직관적입니다. 둘러보세요!" - }, "accountActions": { "message": "계정 작업" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "매우 넓게" + }, + "cannotRemoveViewOnlyCollections": { + "message": "보기 권한만 있는 컬렉션은 제거할 수 없습니다: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 3c81df00f10f..f64eb8b5189f 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Generuoti slaptažodį iš naujo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Patvirtinti tapatybę" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Jūsų saugykla užrakinta. Norėdami tęsti, patikrinkite pagrindinį slaptažodį." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Visada rodyti korteles kaip automatinio pildymo pasiūlymus saugyklos rodinyje" }, "showCardsCurrentTab": { "message": "Rodyti korteles skirtuko puslapyje" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Pateikti kortelių elementų skirtuko puslapyje sąrašą, kad būtų lengva automatiškai užpildyti." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Visada rodyti tapatybes kaip automatinio pildymo pasiūlymus saugyklos rodinyje" }, "showIdentitiesCurrentTab": { "message": "Rodyti tapatybes skirtuko puslapyje" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Įsigyti Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Pradėtas prisijungimas" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Perjungti sutrumpinimą", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importuoti duomenis į Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Apsaugoti LastPass duomenis ir importuoti į Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Išsaugoti kaip neužšifruotą failą", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importuoti į Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importuojama...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Duomenys sėkmingai importuoti.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Klaida importuojant. Išsamesnės informacijos patikrink konsolėje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Importuojant įvyko tinklo klaida.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domeno slapyvardis" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Elemento pavadinimas" }, - "cannotRemoveViewOnlyCollections": { - "message": "Negalite pašalinti kolekcijų su Peržiūrėti tik leidimus: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Negalite pašalinti kolekcijų su Peržiūrėti tik leidimus: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index fc682ced3894..3306cdf5a8f3 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Izveidot paroles vārdkopu" }, + "passwordGenerated": { + "message": "Parole izveidota" + }, + "passphraseGenerated": { + "message": "Paroles vārdkopa izveidota" + }, + "usernameGenerated": { + "message": "Lietotājvārds izveidots" + }, + "emailGenerated": { + "message": "E-pasta adrese izveidota" + }, "regeneratePassword": { "message": "Pārizveidot paroli" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identitātes apliecināšana" }, + "weDontRecognizeThisDevice": { + "message": "Mēs neatpazīstam šo ierīci. Jāievada kods, kas tika nosūtīts e-pastā, lai apliecinātu savu identitāti." + }, + "continueLoggingIn": { + "message": "Turpināt pieteikšanos" + }, "yourVaultIsLocked": { "message": "Glabātava ir aizslēgta. Jāapliecina sava identitāte, lai turpinātu." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "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ā" + "showCardsInVaultViewV2": { + "message": "Glabātavas skatā vienmēr rādīt kartes kā automātiskās aizpildes ieteikumus" }, "showCardsCurrentTab": { "message": "Rādīt kartes cilnes lapā" @@ -995,8 +1013,8 @@ "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ā" + "showIdentitiesInVaultViewV2": { + "message": "Glabātavas skatā vienmēr rādīt identitātes kā automātiskās aizpildes ieteikumus" }, "showIdentitiesCurrentTab": { "message": "Rādīt identitātes cilnes pārskatā" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Iegādāties Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Lietotājvārdu veidotājs" }, + "useThisEmail": { + "message": "Izmantot šo e-pasta adresi" + }, "useThisPassword": { "message": "Izmantot šo paroli" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Automātiskā aizpilde un citas saistītās iespējas šajās tīmekļvietnēs netiks piedāvātas. Ir jāatsvaidzina lapa, lai izmaiņas iedarbotos." }, - "autofillBlockedNotice": { - "message": "Automātiskā aizpilde šajā tīmekļvietnē ir liegta. Šo pārskatīt vai mainīt var iestatījumos." + "autofillBlockedNoticeV2": { + "message": "Automātiskā aizpilde šajā tīmekļvietnē ir liegta." }, - "autofillBlockedTooltip": { - "message": "Automātiskā aizpilde šajā tīmekļvietnē ir liegta. Šo var pārskatīt iestatījumos." + "autofillBlockedNoticeGuidance": { + "message": "To var mainīt iestatījumos" }, "websiteItemLabel": { "message": "Tīmekļvietne $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Uz ierīci ir nosūtīts paziņojums." }, + "notificationSentDevicePart1": { + "message": "Bitwarden jāatslēdz savā ierīcē vai" + }, + "notificationSentDeviceAnchor": { + "message": "tīmekļa lietotnē" + }, + "notificationSentDevicePart2": { + "message": "Pirms apstiprināšanas jāpārliecinās, ka pirkstu nospieduma vārdkopa atbilst zemāk esošajai." + }, "aNotificationWasSentToYourDevice": { "message": "Uz ierīci tika nosūtīts paziņojums" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Lūgums pārliecināties, ka konts ir atslēgts un atpazīšanas vārdkopa ir tāda pati arī otrā ierīcē" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Tiks paziņots, tiklīdz pieprasījums būs apstiprināts" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Uzsākta pieteikšanās" }, + "logInRequestSent": { + "message": "Pieprasījums nosūtīts" + }, "exposedMasterPassword": { "message": "Noplūdusi galvenā parole" }, @@ -3425,38 +3452,6 @@ "message": "Pārslēgt sakļaušanu", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Ievietot datus Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Aizsargāt LastPass datus un ievietot tos Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Saglabāt kā nešifrētu datni", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Ievietot Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Ievieto...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dati veiksmīgi ievietoti.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Kļūda ievietošanā. Jāpārbauda konsole, lai iegūtu vairāk informācijas.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Ievietošanas laikā atgadījās tīkla kļūda.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aizstājdomēns" }, @@ -4008,7 +4003,10 @@ "message": "Piekļuves atslēga noņemta" }, "autofillSuggestions": { - "message": "Ieteikumi automātiskajai aizpildei" + "message": "Automātiskās aizpildes ieteikumi" + }, + "itemSuggestions": { + "message": "Ieteiktie vienumi" }, "autofillSuggestionsTip": { "message": "Saglabāt pieteikšanās vienumi, ko automātiski aizpildīt šajā vietnē" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Vienuma nosaukums" }, - "cannotRemoveViewOnlyCollections": { - "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Apvienība ir atspējota" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Ļoti plats" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lūgums atjaunināt darbvirsmas lietotni" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Lai izmantotu atslēgšanu ar biometriju, lūgums atjaunināt darbvirsmas lietotni vai atspējot atslēgšanu ar pirkstu nospiedumu darbvirsmas iestatījumos." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 4cbbfc46d6ab..75cf2bdd5676 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "പാസ്സ്‌വേഡ് വീണ്ടും സൃഷ്ടിക്കുക" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "തങ്ങളുടെ വാൾട് പൂട്ടിയിരിക്കുന്നു. തുടരുന്നതിന് നിങ്ങളുടെ പ്രാഥമിക പാസ്‌വേഡ് പരിശോധിക്കുക." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "പ്രീമിയം വാങ്ങുക" }, - "premiumPurchaseAlert": { - "message": "നിങ്ങൾക്ക് bitwarden.com വെബ് വാൾട്ടിൽ പ്രീമിയം അംഗത്വം വാങ്ങാം. നിങ്ങൾക്ക് ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index cbb0b1bdf1a1..622cb1be6392 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "पासवर्ड पुनर्जनित करा" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "ओळख सत्यापित करा" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "तुमची तिजोरीला कुलूप लावले आहे. पुढे जाण्यासाठी तुमची ओळख सत्यापित करा." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index e34751eea7dd..800523bdc2ec 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 3a12c9ae4f42..ce50ff4f90a4 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -7,7 +7,7 @@ "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": "Hjemme, på jobben eller på farten sikrer Bitwarden enkelt alle dine passord, passnøkler og sensitiv informasjon", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -81,10 +81,10 @@ "message": "Et hint for hovedpassordet (valgfritt)" }, "joinOrganization": { - "message": "Join organization" + "message": "Bli med i organisasjonen" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Bli med i $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -141,7 +141,7 @@ "message": "Kopiér navn" }, "copyCompany": { - "message": "Copy company" + "message": "Kopiér firma" }, "copySSN": { "message": "Kopiér fødselsnummer" @@ -281,13 +281,13 @@ "message": "Endre hovedpassordet" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Vil du fortsette til nettappen?" }, "continueToWebAppDesc": { "message": "Explore more features of your Bitwarden account on the web app." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Vil du fortsette til Hjelpesenteret?" }, "continueToHelpCenterDesc": { "message": "Learn more about how to use Bitwarden on the Help Center." @@ -299,7 +299,7 @@ "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Du kan endre hovedpassordet ditt i Bitwardens nettapp." }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", @@ -322,16 +322,16 @@ "message": "Om" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Mer fra Bitwarden" }, "continueToBitwardenDotCom": { "message": "Vil du fortsette til bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden for bedrifter" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Bitwarden-autentiserer" }, "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" @@ -431,7 +431,7 @@ "message": "Generer automatisk sterke og unike passord for dine innlogginger." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwardens nett-app" }, "importItems": { "message": "Importer elementer" @@ -443,7 +443,19 @@ "message": "Generer et passord" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Generér passordfrase" + }, + "passwordGenerated": { + "message": "Passord generert" + }, + "passphraseGenerated": { + "message": "Passordfrase generert" + }, + "usernameGenerated": { + "message": "Brukernavn generert" + }, + "emailGenerated": { + "message": "E-postadresse generert" }, "regeneratePassword": { "message": "Omgenerer et passord" @@ -555,7 +567,7 @@ "message": "Passord" }, "totp": { - "message": "Authenticator secret" + "message": "Autentiseringsnøkkel" }, "passphrase": { "message": "Passfrase" @@ -621,7 +633,7 @@ "message": "Annet" }, "unlockMethods": { - "message": "Unlock options" + "message": "Opplåsingsalternativer" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." @@ -630,10 +642,10 @@ "message": "Set up an unlock method in Settings" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Tidsavbrudd for økten" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Tidsavbrudd for hvelvet" }, "otherOptions": { "message": "Andre valg" @@ -647,14 +659,20 @@ "verifyIdentity": { "message": "Bekreft identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Fortsett innloggingen" + }, "yourVaultIsLocked": { "message": "Hvelvet ditt er låst. Kontroller hovedpassordet ditt for å fortsette." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Hvelvet ditt er låst" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Kontoen din er låst" }, "or": { "message": "eller" @@ -779,10 +797,10 @@ "message": "Din nye konto har blitt opprettet! Du kan nå logge på." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "Den nye kontoen din er opprettet!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Du har blitt logget inn!" }, "youSuccessfullyLoggedIn": { "message": "Du har vellykket logget inn" @@ -843,7 +861,7 @@ "message": "Logget av" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Du har blitt logget ut av kontoen din." }, "loginExpired": { "message": "Din innloggingsøkt har utløpt." @@ -855,7 +873,7 @@ "message": "Logg inn på Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "Start registreringen på nytt" }, "expiredLink": { "message": "Utløpt lenke" @@ -891,7 +909,7 @@ "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Vil du fortsette til nettappen?" }, "editedFolder": { "message": "Redigerte mappen" @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Vis kort på fanesiden" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Vis kortelementer på fanesiden for lett auto-utfylling." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanesiden" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Kjøp Premium" }, - "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." }, @@ -1275,7 +1290,7 @@ "message": "Takk for at du støtter Bitwarden." }, "premiumFeatures": { - "message": "Upgrade to Premium and receive:" + "message": "Oppgrader til Premium og motta:" }, "premiumPrice": { "message": "Og alt det for %price%/år!", @@ -1462,29 +1477,29 @@ "message": "Miljø-nettadressene har blitt lagret." }, "showAutoFillMenuOnFormFields": { - "message": "Show autofill menu on form fields", + "message": "Vis autoutfyll-menyen i tekstbokser", "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { "message": "Autoutfyllingsforslag" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Vis autoutfyll-forslag i tekstbokser" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "Vis identiteter som forslag" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "Vis kort som forslag" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Vis forslag når ikonet er valgt" }, "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser's built in password manager settings to avoid conflicts." + "message": "Skru av din nettlesers innebygde passordbehandler for å unngå konflikter." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Rediger nettleserinnstillingene." @@ -1523,7 +1538,7 @@ "message": "Standard autofyll innstilling for innloggingselementer" }, "defaultAutoFillOnPageLoadDesc": { - "message": "Etter aktivering av auto-utfylling på sidelasser, kan du aktivere eller deaktivere funksjonen for individuelle innloggingselementer. Dette er standardinnstillingen for innloggingselementer som ikke er satt opp separat." + "message": "Du kan skru av auto-utfylling ved sideinnlastinger for individuelle innloggingsgjenstander fra gjenstandens «Redigér»-visning." }, "itemAutoFillOnPageLoad": { "message": "Auto-utfyll på sideinnlastning (hvis aktivert i Alternativer)" @@ -1583,7 +1598,7 @@ "message": "Boolsk verdi" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Avkryssingsboks" }, "cfTypeLinked": { "message": "Tilkoblet", @@ -1804,7 +1819,7 @@ "message": "Generatorhistorikk" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Tøm generatorhistorikk" }, "cleargGeneratorHistoryDescription": { "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" @@ -1816,7 +1831,7 @@ "message": "Samlinger" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ samlinger", "placeholders": { "count": { "content": "$1", @@ -1872,7 +1887,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Grunndomene (anbefalt)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1932,7 +1947,7 @@ "message": "Ingenting å vise" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Du har ikke generert noe i det siste" }, "remove": { "message": "Fjern" @@ -2002,7 +2017,7 @@ "message": "Angi PIN-koden din for å låse opp Bitwarden. PIN-innstillingene tilbakestilles hvis du logger deg helt ut av programmet." }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "PIN-koden din vil bli brukt til å låse opp Bitwarden i stedet for hovedpassordet ditt. PIN-koden din tilbakestilles hvis du noen gang logger deg helt ut av Bitwarden." }, "pinRequired": { "message": "PIN-kode er påkrevd." @@ -2011,7 +2026,7 @@ "message": "Ugyldig PIN-kode." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "For mange ugyldige PIN-kodeforsøk. Logger ut." }, "unlockWithBiometrics": { "message": "Lås opp med biometri" @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Brukernavngenerator" }, + "useThisEmail": { + "message": "Bruk denne E-postadressen" + }, "useThisPassword": { "message": "Bruk dette passordet" }, @@ -2060,14 +2078,14 @@ "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", + "message": "for å lage et sterkt og unikt passord", "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" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "Handling ved tidsavbrudd" }, "lock": { "message": "Lås", @@ -2117,7 +2135,7 @@ "message": "Autoutfylt element" }, "insecurePageWarning": { - "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." + "message": "Advarsel: Dette er en usikret HTTP-side, og all informasjon du sender inn kan potensielt bli sett og endret av andre. Denne påloggingen ble opprinnelig lagret på et sikkert (HTTPS) nettsted." }, "insecurePageWarningFillPrompt": { "message": "Ønsker du likevel å fylle ut denne innloggingen?" @@ -2288,7 +2306,7 @@ "message": "Please unlock this user in the desktop application and try again." }, "biometricsNotAvailableTitle": { - "message": "Biometric unlock unavailable" + "message": "Biometrisk opplåsing er utilgjengelig" }, "biometricsNotAvailableDesc": { "message": "Biometric unlock is currently unavailable. Please try again later." @@ -2339,14 +2357,14 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Endre dette i innstillingene" }, "websiteItemLabel": { - "message": "Website $number$ (URI)", + "message": "Nettsted $number$ (URİ)", "placeholders": { "number": { "content": "$1", @@ -2373,11 +2391,11 @@ "message": "Begrens visninger" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "Ingen kan se denne Send-en etter at grensen er nådd.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ visninger igjen", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2391,7 +2409,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Send details", + "message": "Send-detaljer", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -2408,7 +2426,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Hide text by default" + "message": "Skjul tekst som standard" }, "expired": { "message": "Utløpt" @@ -2488,7 +2506,7 @@ "message": "Egendefinert" }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Legg til et valgfritt passord for at mottakerne skal få tilgang til denne Send-en.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { @@ -2543,7 +2561,7 @@ } }, "sendLinkCopied": { - "message": "Send link copied", + "message": "Send-lenken ble kopiert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { @@ -2551,7 +2569,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogText": { - "message": "Pop out extension?", + "message": "Vil du sprette ut utvidelsen?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { @@ -2568,7 +2586,7 @@ "message": "For å velge en fil med Safari, popp ut i et nytt vindu ved å klikke på dette banneret." }, "popOut": { - "message": "Pop out" + "message": "Sprett ut" }, "sendFileCalloutHeader": { "message": "Før du starter" @@ -2589,7 +2607,7 @@ "message": "Det oppstod en feil ved lagring av slettingen og utløpsdatoene." }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "Skjul E-postadressen din fra seere." }, "passwordPrompt": { "message": "Forespørsel om hovedpassord på nytt" @@ -2815,14 +2833,14 @@ "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "for å unngå ytterligere datatap.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generer brukernavn" }, "generateEmail": { - "message": "Generate email" + "message": "Generér E-post" }, "spinboxBoundariesHint": { "message": "Verdien må være mellom $MIN$ og $MAX$.", @@ -2839,7 +2857,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Bruk minst $RECOMMENDED$ tegn for å generere et sterkt passord.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2894,7 +2912,7 @@ "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Velg et domene som støttes av den valgte tjenesten", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -2926,7 +2944,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Ugyldig $SERVICENAME$-API-sjetong", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2936,7 +2954,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Ugyldig $SERVICENAME$-API-sjetong: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2980,7 +2998,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Ukjent $SERVICENAME$-feil oppstod.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Et varsel er sendt til enheten din." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "nett-app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Et varsel ble sendt til enheten din" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3118,7 +3142,10 @@ "message": "Trenger du et annet alternativ?" }, "loginInitiated": { - "message": "Login initiated" + "message": "Innlogging igangsatt" + }, + "logInRequestSent": { + "message": "Forespørsel sendt" }, "exposedMasterPassword": { "message": "Eksponert hovedpassord" @@ -3178,10 +3205,10 @@ "message": "Autofill shortcut" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "Endre snarvei" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Behandle snarveier" }, "autofillShortcut": { "message": "Auto-utfyll tastatursnarvei" @@ -3190,7 +3217,7 @@ "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "Autoutfyll-snarveien for pålogging er $COMMAND$. Håndter alle snarveiene i nettleserens innstillinger.", "placeholders": { "command": { "content": "$1", @@ -3244,16 +3271,16 @@ "message": "Oppretter en konto på" }, "checkYourEmail": { - "message": "Check your email" + "message": "Sjekk E-postinnboksen din" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Følg lenken i E-postadressen som ble sendt til" }, "andContinueCreatingYourAccount": { "message": "and continue creating your account." }, "noEmail": { - "message": "No email?" + "message": "Ingen E-post?" }, "goBack": { "message": "Gå tilbake" @@ -3302,11 +3329,11 @@ "message": "Enheten er betrodd" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "Ingen aktive Send", "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.", + "message": "Bruk Send til å dele kryptert informasjon med noen.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3383,10 +3410,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 felt trenger din oppmerksomhet." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ felter trenger din oppmerksomhet.", "placeholders": { "count": { "content": "$1", @@ -3422,41 +3449,9 @@ "message": "Undermeny" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "Utvid eller klapp sammen", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Vil du importere dataene dine til Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Lagre som ukryptert fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importer til Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerer …", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dataene ble vellykket importert!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Feil under importering. Sjekk loggkonsollen for detaljer.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias-domene" }, @@ -3660,7 +3655,7 @@ "message": "Popout extension" }, "launchDuo": { - "message": "Launch Duo" + "message": "Start Duo" }, "importFormatError": { "message": "Data is not formatted correctly. Please check your import file and try again." @@ -3734,13 +3729,13 @@ "message": "Bekreft filpassord" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Hvelvdataen ble eksportert" }, "typePasskey": { "message": "Passnøkkel" }, "accessing": { - "message": "Accessing" + "message": "Logger inn på" }, "loggedInExclamation": { "message": "Innlogget!" @@ -3770,7 +3765,7 @@ "message": "No matching logins for this site" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Søk eller lagre passnøkkelen som en ny innlogging" }, "confirm": { "message": "Bekreft" @@ -3788,7 +3783,7 @@ "message": "Choose a passkey to log in with" }, "passkeyItem": { - "message": "Passkey Item" + "message": "Passkode-gjenstand" }, "overwritePasskey": { "message": "Overwrite passkey?" @@ -3935,7 +3930,7 @@ "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "Vil du fortsette til Hjelpesenteret?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { @@ -3955,7 +3950,7 @@ "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": "Vil du sette Bitwarden som din standard passordbehandler?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autoutfyllingsforslag" }, + "itemSuggestions": { + "message": "Foreslåtte gjenstander" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4023,7 +4021,7 @@ "message": "Clear filters or try another search term" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Kopiér info - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4033,7 +4031,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "Kopiér notat - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4083,7 +4081,7 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "Ingen verdier å kopiere" }, "assignToCollections": { "message": "Legg til i samlinger" @@ -4092,10 +4090,10 @@ "message": "Copy email" }, "copyPhone": { - "message": "Copy phone" + "message": "Kopiér telefonnummer" }, "copyAddress": { - "message": "Copy address" + "message": "Kopiér adresse" }, "adminConsole": { "message": "Admin Console" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Gjenstandens navn" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4189,13 +4178,13 @@ "message": "Nyligst redigert" }, "ownerYou": { - "message": "Owner: You" + "message": "Eier: Du" }, "linked": { "message": "Tilknyttet" }, "copySuccessful": { - "message": "Copy Successful" + "message": "Kopiering lyktes" }, "upload": { "message": "Last opp" @@ -4207,7 +4196,7 @@ "message": "Maksimal filstørrelse er 500 MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Slett $NAME$-vedlegget", "placeholders": { "name": { "content": "$1", @@ -4283,10 +4272,10 @@ "message": "Autoutfyllings-innstillinger" }, "websiteUri": { - "message": "Website (URI)" + "message": "Nettsted (URİ)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "Nettsted (URİ) $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": { @@ -4414,7 +4403,7 @@ "message": "Feltetikett" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "Bruk tekstfelter for data som sikkerhetsspørsmål" }, "hiddenHelpText": { "message": "Use hidden fields for sensitive data like a password" @@ -4432,7 +4421,7 @@ "message": "Rediger felt" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "Rediger $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4485,7 +4474,7 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Velg samlinger å tilordne" }, "personalItemTransferWarningSingular": { "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." @@ -4525,10 +4514,10 @@ "message": "Successfully assigned collections" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "Du har ikke valgt noe." }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "De valgte gjenstandene ble flyttet til $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4537,7 +4526,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "Gjenstandene ble flyttet til $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4546,7 +4535,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "Gjenstanden ble flyttet til $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4586,20 +4575,14 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden har fått et 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": "Account actions" + "message": "Kontohandlinger" }, "showNumberOfAutofillSuggestions": { "message": "Show number of login autofill suggestions on extension icon" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Vis hurtigkopieringshandlinger i hvelvet" }, "systemDefault": { "message": "Systemforvalg" @@ -4635,7 +4618,7 @@ "message": "Prøv igjen" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "Minste egendefinerte tidsavbrudd er 1 minutt." }, "additionalContentAvailable": { "message": "Ytterligere innhold er tilgjengelig" @@ -4656,10 +4639,10 @@ "message": "Ingen gjenstander i papirkurven" }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "Gjenstander du sletter, vises her og slettes permanent etter 30 dager" }, "trashWarning": { - "message": "Items that have been in trash more than 30 days will automatically be deleted" + "message": "Gjenstander som har ligget i papirkurven i mer enn 30 dager, blir automatisk slettet" }, "restore": { "message": "Gjenopprett" @@ -4674,7 +4657,7 @@ "message": "Biometric unlock is unavailable because PIN or password unlock is required first." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Biometrisk opplåsing er utilgjengelig for øyeblikket." }, "biometricsStatusHelptextAutoSetupNeeded": { "message": "Biometric unlock is unavailable due to misconfigured system files." @@ -4701,11 +4684,11 @@ "message": "Autentiserer" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "Fyll inn generert passord", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "Passord ble generert på nytt", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { @@ -4872,7 +4855,7 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Minn meg på det senere" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -4890,7 +4873,7 @@ "message": "Yes, I can reliably access my email" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Slå på 2-trinnsinnlogging" }, "changeAcctEmail": { "message": "Endre kontoens E-postadresse" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Ekstra bred" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Vennligst oppdater skrivebordsprogrammet ditt" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "For å bruke biometrisk opplåsing, må du oppdatere skrivebordsprogrammet eller skru av fingeravtrykksopplåsing i skrivebordsinnstillingene." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index e34751eea7dd..800523bdc2ec 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 0112ded19831..076c35732c03 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Thuis, op werk of onderweg. Bitwarden beveiligt makkelijk all je wachtwoorden, passkeys en gevoelige informatie", + "message": "Thuis, op werk of onderweg. Bitwarden beveiligt makkelijk al je wachtwoorden, passkeys en gevoelige informatie", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -38,7 +38,7 @@ "message": "Rond het aanmaken van je account af met het instellen van een wachtwoord" }, "enterpriseSingleSignOn": { - "message": "Single sign-on voor bedrijven" + "message": "Enterprise Single Sign-On" }, "cancel": { "message": "Annuleren" @@ -409,7 +409,7 @@ "message": "Ontdek Bitwarden community-forums" }, "contactSupport": { - "message": "Contacteer Bitwarden support" + "message": "Contacteer Bitwarden ondersteuning" }, "sync": { "message": "Synchroniseren" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Wachtwoordzin genereren" }, + "passwordGenerated": { + "message": "Wachtwoord gegenereerd" + }, + "passphraseGenerated": { + "message": "Wachtwoorden gegenereerd" + }, + "usernameGenerated": { + "message": "Gebruikersnaam gegenereerd" + }, + "emailGenerated": { + "message": "E-mail gegenereerd" + }, "regeneratePassword": { "message": "Wachtwoord opnieuw genereren" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identiteit verifiëren" }, + "weDontRecognizeThisDevice": { + "message": "We herkennen dit apparaat niet. Voer de code in die naar je e-mail is verzonden om je identiteit te verifiëren." + }, + "continueLoggingIn": { + "message": "Doorgaan met inloggen" + }, "yourVaultIsLocked": { "message": "Je kluis is vergrendeld. Bevestig je identiteit om door te gaan." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "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" + "showCardsInVaultViewV2": { + "message": "Kaarten altijd als auto-invullen suggesties in de kluisweergave weergeven" }, "showCardsCurrentTab": { "message": "Kaarten weergeven op tabpagina" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kaartenitems weergeven op de tabpagina voor gemakkelijk automatisch invullen." }, - "showIdentitiesInVaultView": { - "message": "Identiteiten als Autofill-suggesties in de kluisweergave weergeven" + "showIdentitiesInVaultViewV2": { + "message": "Identiteiten altijd als auto-invullen suggesties in de kluisweergave weergeven" }, "showIdentitiesCurrentTab": { "message": "Identiteiten weergeven op tabpagina" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium aanschaffen" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Gebruikersnaamgenerator" }, + "useThisEmail": { + "message": "Dit e-mailadres gebruiken" + }, "useThisPassword": { "message": "Dit wachtwoord gebruiken" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill en andere gerelateerde functies worden niet aangeboden voor deze websites. Vernieuw de pagina om de wijzigingen toe te passen." }, - "autofillBlockedNotice": { - "message": "Automatisch invullen is geblokkeerd voor deze website. Bekijk of verander dit in de instellingen." + "autofillBlockedNoticeV2": { + "message": "Autofill is geblokkeerd voor deze website." }, - "autofillBlockedTooltip": { - "message": "Automatisch invullen is geblokkeerd voor deze website. Bekijk in de instellingen." + "autofillBlockedNoticeGuidance": { + "message": "Dit aanpassen in instellingen" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Er is een melding naar je apparaat verzonden." }, + "notificationSentDevicePart1": { + "message": "Ontgrendel Bitwarden op je apparaat of op de" + }, + "notificationSentDeviceAnchor": { + "message": "webapp" + }, + "notificationSentDevicePart2": { + "message": "Zorg ervoor dat de Vingerafdrukzin overeenkomt met de onderstaande voor je deze goedkeurt." + }, "aNotificationWasSentToYourDevice": { "message": "Er is een melding naar je apparaat verzonden" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Zorg ervoor dat je kluis is ontgrendeld en de vingerafdrukzin hetzelfde is op het andere apparaat" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Je krijgt een melding zodra de aanvraag is goedgekeurd" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Inloggen gestart" }, + "logInRequestSent": { + "message": "Verzoek verzonden" + }, "exposedMasterPassword": { "message": "Gelekt hoofdwachtwoord" }, @@ -3425,38 +3452,6 @@ "message": "In-/Uitklappen", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Gegevens importeren naar Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Je LastPass-gegevens beschermen en naar Bitwarden importeren?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Als niet-versleuteld bestand opslaan", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Naar Bitwarden importeren", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importeren…", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Gegevens succesvol geïmporteerd!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fout bij importeren. Controleer console voor meer informatie.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netwerkfout opgetreden tijdens het importeren.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomein" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Suggesties automatisch invullen" }, + "itemSuggestions": { + "message": "Voorgestelde items" + }, "autofillSuggestionsTip": { "message": "Inlogitem opslaan voor automatisch invullen op deze site" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Itemnaam" }, - "cannotRemoveViewOnlyCollections": { - "message": "Je kunt verzamelingen niet verwijderen met alleen rechten voor weergeven: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organisatie is gedeactiveerd" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra breed" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Je kunt verzamelingen niet verwijderen met alleen rechten voor weergeven: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Werk je desktopapplicatie bij" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Als je biometrische gegevens wilt gebruiken, moet je de desktopapplicatie bijwerken of vingerafdrukontgrendeling uitschakelen in de instellingen van de desktopapplicatie." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index e34751eea7dd..800523bdc2ec 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index e34751eea7dd..800523bdc2ec 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 4b7d3a19fc4d..232ec85c80a2 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -20,10 +20,10 @@ "message": "Utwórz konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nowy użytkownik Bitwarden?" }, "logInWithPasskey": { - "message": "Zaloguj się używając passkey" + "message": "Zaloguj się używając klucza dostępu" }, "useSingleSignOn": { "message": "Użyj jednokrotnego logowania" @@ -120,7 +120,7 @@ "message": "Kopiuj hasło" }, "copyPassphrase": { - "message": "Kopiuj frazę bezpieczeństwa" + "message": "Skopiuj hasło wyrazowe" }, "copyNote": { "message": "Kopiuj notatkę" @@ -147,7 +147,7 @@ "message": "Kopiuj numer PESEL" }, "copyPassportNumber": { - "message": "Kopiuj numer paszportu" + "message": "Skopiuj numer paszportu" }, "copyLicenseNumber": { "message": "Kopiuj numer licencji" @@ -443,7 +443,19 @@ "message": "Wygeneruj hasło" }, "generatePassphrase": { - "message": "Wygenruj frazę zabezpieczającą" + "message": "Wygeneruj hasło wyrazowe" + }, + "passwordGenerated": { + "message": "Hasło zostało wygenerowane" + }, + "passphraseGenerated": { + "message": "Hasło wyrazowe zostało wygenerowane" + }, + "usernameGenerated": { + "message": "Nazwa użytkownika została wygenerowana" + }, + "emailGenerated": { + "message": "E-mail został wygenerowany" }, "regeneratePassword": { "message": "Wygeneruj ponownie hasło" @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Zweryfikuj tożsamość" }, + "weDontRecognizeThisDevice": { + "message": "Nie rozpoznajemy tego urządzenia. Wpisz kod wysłany na Twój e-mail, aby zweryfikować tożsamość." + }, + "continueLoggingIn": { + "message": "Kontynuuj logowanie" + }, "yourVaultIsLocked": { "message": "Sejf jest zablokowany. Zweryfikuj swoją tożsamość, aby kontynuować." }, @@ -788,7 +806,7 @@ "message": "Zalogowałeś się pomyślnie" }, "youMayCloseThisWindow": { - "message": "Możesz zamknąć to okno." + "message": "Możesz zamknąć to okno" }, "masterPassSent": { "message": "Wysłaliśmy Tobie wiadomość e-mail z podpowiedzią do hasła głównego." @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Zawsze pokazuj karty jako sugestie autouzupełniania w widoku sejfu" }, "showCardsCurrentTab": { "message": "Pokaż karty na stronie głównej" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." }, - "showIdentitiesInVaultView": { - "message": "Pokaż tożsamości jako sugestie autouzupełniania w widoku sejfu" + "showIdentitiesInVaultViewV2": { + "message": "Zawsze pokazuj tożsamości jako sugestie autouzupełniania w widoku sejfu" }, "showIdentitiesCurrentTab": { "message": "Pokaż tożsamości na stronie głównej" @@ -1031,10 +1049,10 @@ "message": "Poproś o aktualizację hasła, gdy zmiana zostanie wykryta na stronie. Dotyczy wszystkich zalogowanych kont." }, "enableUsePasskeys": { - "message": "Pytaj o zapisywanie i używanie passkey" + "message": "Pytaj o zapisywanie i używanie kluczy dostępu" }, "usePasskeysDesc": { - "message": "Pytaj o zapisywanie nowych passkey albo danych logowania z passkey w Twoim sejfie. Dotyczy wszystkich zalogowanych kont." + "message": "Pytaj o zapisywanie nowych kluczy dostępu albo danych logowania z kluczy w Twoim sejfie. Dotyczy wszystkich zalogowanych kont." }, "notificationChangeDesc": { "message": "Czy chcesz zaktualizować to hasło w Bitwarden?" @@ -1055,7 +1073,7 @@ "message": "Pokaż opcje menu kontekstowego" }, "contextMenuItemDesc": { - "message": "Użyj drugiego kliknięcia, aby uzyskać dostęp do generowania haseł i pasujących danych logowania do witryny. " + "message": "Użyj drugiego kliknięcia, aby uzyskać dostęp do generowania haseł i pasujących danych logowania do witryny." }, "contextMenuItemDescAlt": { "message": "Użyj drugiego kliknięcia, aby uzyskać dostęp do generowania haseł i pasujących danych logowania do witryny. Dotyczy wszystkich zalogowanych kont." @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Kup konto Premium" }, - "premiumPurchaseAlert": { - "message": "Konto Premium możesz zakupić na stronie sejfu bitwarden.com. Czy chcesz otworzyć tę stronę?" - }, "premiumPurchaseAlertV2": { "message": "Możesz kupić Premium w ustawieniach konta w aplikacji internetowej Bitwarden." }, @@ -1320,7 +1335,7 @@ "message": "Limit czasu uwierzytelniania" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Upłynął limit czasu uwierzytelniania. Uruchom ponownie proces logowania." }, "enterVerificationCodeEmail": { "message": "Wpisz 6-cyfrowy kod weryfikacyjny, który został przesłany na adres $EMAIL$.", @@ -1478,7 +1493,7 @@ "message": "Pokazuj karty jako sugestie" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Wyświetlaj sugestie kiedy ikona jest zaznaczona" + "message": "Wyświetlaj sugestie, kiedy ikona jest zaznaczona" }, "showInlineMenuOnFormFieldsDescAlt": { "message": "Dotyczy wszystkich zalogowanych kont." @@ -1523,7 +1538,7 @@ "message": "Domyślne ustawienie autouzupełniania" }, "defaultAutoFillOnPageLoadDesc": { - "message": "Po włączeniu autouzupełnianiu po załadowaniu strony, możesz włączyć lub wyłączyć tę funkcję dla poszczególnych wpisów." + "message": "Po włączeniu autouzupełnianiu po załadowaniu strony możesz włączyć lub wyłączyć tę funkcję dla poszczególnych wpisów." }, "itemAutoFillOnPageLoad": { "message": "Automatycznie uzupełniaj po załadowaniu strony (jeśli włączono w opcjach)" @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generator nazw użytkownika" }, + "useThisEmail": { + "message": "Użyj tego adresu e-mail" + }, "useThisPassword": { "message": "Użyj tego hasła" }, @@ -2114,7 +2132,7 @@ "message": "URI został zapisany i automatycznie uzupełniony" }, "autoFillSuccess": { - "message": "Element został automatycznie uzupełniony" + "message": "Element został automatycznie uzupełniony " }, "insecurePageWarning": { "message": "Ostrzeżenie: Jest to niezabezpieczona strona HTTP i wszelkie przekazane informacje mogą być potencjalnie widoczne i zmienione przez innych. Ten login został pierwotnie zapisany na stronie bezpiecznej (HTTPS)." @@ -2123,7 +2141,7 @@ "message": "Nadal chcesz uzupełnić ten login?" }, "autofillIframeWarning": { - "message": "Formularz jest hostowany przez inną domenę niż zapisany adres URI dla tego loginu. Wybierz OK, aby i tak automatycznie wypełnić lub anuluj aby zatrzymać." + "message": "Formularz jest hostowany przez inną domenę niż zapisany adres URI dla tego loginu. Wybierz OK, aby i tak automatycznie wypełnić lub anuluj, aby zatrzymać." }, "autofillIframeWarningTip": { "message": "Aby zapobiec temu ostrzeżeniu w przyszłości, zapisz ten URI, $HOSTNAME$, dla tej witryny.", @@ -2267,7 +2285,7 @@ "message": "Klucz biometryczny jest niepoprawny" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Odblokowanie biometryczne nie powiodło się. Sekretny klucz biometryczny nie odblokował sejfu. Spróbuj skonfigurować biometrię ponownie." + "message": "Odblokowanie biometryczne się nie powiodło. Sekretny klucz biometryczny nie odblokował sejfu. Spróbuj skonfigurować biometrię ponownie." }, "biometricsNotEnabledTitle": { "message": "Dane biometryczne są wyłączone" @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Zablokowane domeny" }, "excludedDomains": { "message": "Wykluczone domeny" @@ -2337,13 +2355,13 @@ "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." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Autouzupełnianie i inne powiązane funkcje nie będą oferowane dla tych stron. Aby zmiany zaczęły obowiązywać, musisz odświeżyć stronę." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autouzupełnianie jest zablokowane dla tej witryny." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Zmień to w ustawieniach" }, "websiteItemLabel": { "message": "Strona internetowa $number$ (URI)", @@ -2364,7 +2382,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Zmiany w zablokowanych domenach zapisane" }, "excludedDomainsSavedSuccess": { "message": "Zmiany w wykluczonych domenach zapisane" @@ -2555,7 +2573,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "To create a file Send, you need to pop out the extension to a new window.", + "message": "Aby utworzyć plik Send, musisz wysunąć rozszerzenie do nowego okna.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -2568,7 +2586,7 @@ "message": "Aby wybrać plik za pomocą przeglądarki Safari, otwórz rozszerzenie w nowym oknie." }, "popOut": { - "message": "Pop out" + "message": "Odepnij" }, "sendFileCalloutHeader": { "message": "Zanim zaczniesz" @@ -2805,17 +2823,17 @@ "message": "Błąd" }, "decryptionError": { - "message": "Decryption error" + "message": "Błąd odszyfrowywania" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden nie mógł odszyfrować elementów sejfu wymienionych poniżej." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Skontaktuj się z działem obsługi klienta,", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "aby uniknąć dalszej utraty danych.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -3105,14 +3123,20 @@ "notificationSentDevice": { "message": "Powiadomienie zostało wysłane na urządzenie." }, + "notificationSentDevicePart1": { + "message": "Odblokuj Bitwarden na swoim urządzeniu lub w" + }, + "notificationSentDeviceAnchor": { + "message": "aplikacji internetowej" + }, + "notificationSentDevicePart2": { + "message": "Upewnij się, że fraza odcisku palca zgadza się z tą poniżej, zanim zatwierdzisz." + }, "aNotificationWasSentToYourDevice": { "message": "Powiadomienie zostało wysłane na twoje urządzenie" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Zostaniesz powiadomiony po zatwierdzeniu prośby" }, "needAnotherOptionV1": { "message": "Potrzebujesz innego sposobu?" @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Logowanie rozpoczęte" }, + "logInRequestSent": { + "message": "Żądanie wysłane" + }, "exposedMasterPassword": { "message": "Ujawnione hasło główne" }, @@ -3151,7 +3178,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Twoja organizacji włączyła autouzupełnianie podczas wczytywania strony." + "message": "Twoja organizacja włączyła autouzupełnianie podczas wczytywania strony." }, "howToAutofill": { "message": "Jak autouzupełniać" @@ -3226,7 +3253,7 @@ "message": "Zapamiętaj to urządzenie" }, "uncheckIfPublicDevice": { - "message": "Odznacz jeśli używasz publicznego urządzenia" + "message": "Odznacz, jeśli używasz publicznego urządzenia" }, "approveFromYourOtherDevice": { "message": "Zatwierdź z innego twojego urządzenia" @@ -3425,43 +3452,11 @@ "message": "Zwiń/rozwiń", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Zaimportować Twoje dane do Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Ochronić Twoje dane LastPass i zaimportować do Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Zapisz jako niezaszyfrowany plik", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importuj do Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importowanie...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dane pomyślnie zaimportowane!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Błąd podczas importowania. Sprawdź konsolę, aby uzyskać szczegółowe informacje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Wystąpił błąd sieci podczas importu.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domena aliasu" }, "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.", + "message": "Elementy z pytaniem o hasło główne nie mogą być autouzupełniane przy wczytywaniu strony. Autouzupełnianie podczas wczytywania strony zostało wyłączone.", "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { @@ -3507,7 +3502,7 @@ "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "Kod weryfikacyjny jednorazowego hasła oparty na czasie", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { @@ -3737,7 +3732,7 @@ "message": "Dane sejfu zostały wyeksportowane" }, "typePasskey": { - "message": "Passkey" + "message": "Klucz dostępu" }, "accessing": { "message": "Uzyskiwanie dostępu" @@ -3746,22 +3741,22 @@ "message": "Zalogowano!" }, "passkeyNotCopied": { - "message": "Passkey nie zostanie skopiowany" + "message": "Klucz dostępu nie zostanie skopiowany" }, "passkeyNotCopiedAlert": { - "message": "Passkey nie zostanie skopiowane do sklonowanego elementu. Czy chcesz kontynuować klonowanie tego elementu?" + "message": "Klucz dostępu nie zostanie skopiowany do sklonowanego elementu. Czy chcesz kontynuować klonowanie tego elementu?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Weryfikacja jest wymagana przez stronę inicjującą. Ta funkcja nie jest jeszcze zaimplementowana dla kont bez hasła głównego." }, "logInWithPasskeyQuestion": { - "message": "Zaloguj się za pomocą passkey?" + "message": "Zalogować za pomocą klucza dostępu?" }, "passkeyAlreadyExists": { - "message": "Passkey już istnieje dla tej aplikacji." + "message": "Klucz dostępu już istnieje dla tej aplikacji." }, "noPasskeysFoundForThisApplication": { - "message": "Nie znaleziono passkey'a dla tej aplikacji." + "message": "Nie znaleziono klucza dostępu dla tej aplikacji." }, "noMatchingPasskeyLogin": { "message": "Nie masz pasujących danych logowania do tej witryny." @@ -3770,37 +3765,37 @@ "message": "Brak pasujących loginów dla tej witryny" }, "searchSavePasskeyNewLogin": { - "message": "Wyszukaj alb zapisz passkey jako nowy login" + "message": "Wyszukaj albo zapisz klucz dostępu jako nowy login" }, "confirm": { "message": "Potwierdź" }, "savePasskey": { - "message": "Zapisz passkey" + "message": "Zapisz klucz dostępu" }, "savePasskeyNewLogin": { - "message": "Zapisz passkey jako nowe dane logowania" + "message": "Zapisz klucz dostępu jako nowe dane logowania" }, "chooseCipherForPasskeySave": { - "message": "Wybierz dane logowania do których przypisać passkey" + "message": "Wybierz dane logowania, do których przypisać klucz dostępu" }, "chooseCipherForPasskeyAuth": { - "message": "Wybierz passkey żeby się zalogować" + "message": "Wybierz klucz dostępu, żeby się zalogować" }, "passkeyItem": { - "message": "Element Passkey" + "message": "Element klucza dostępu" }, "overwritePasskey": { - "message": "Zastąpić passkey?" + "message": "Zastąpić klucz dostępu?" }, "overwritePasskeyAlert": { - "message": "Ten element zawiera już passkey. Czy na pewno chcesz nadpisać bieżący passkey?" + "message": "Ten element zawiera już klucz dostępu. Czy na pewno chcesz nadpisać bieżący klucza dostępu?" }, "featureNotSupported": { "message": "Funkcja nie jest jeszcze obsługiwana" }, "yourPasskeyIsLocked": { - "message": "Wymagane uwierzytelnienie aby używać passkey. Sprawdź swoją tożsamość, aby kontynuować." + "message": "Wymagane uwierzytelnienie, aby używać klucza dostępu. Sprawdź swoją tożsamość, aby kontynuować." }, "multifactorAuthenticationCancelled": { "message": "Uwierzytelnianie wieloskładnikowe zostało anulowane" @@ -4002,13 +3997,16 @@ "message": "Sukces" }, "removePasskey": { - "message": "Usuń passkey" + "message": "Usuń klucz dostępu" }, "passkeyRemoved": { - "message": "Passkey został usunięty" + "message": "Klucz dostępu został usunięty" }, "autofillSuggestions": { - "message": "Sugestie autouzupełnienia" + "message": "Sugestie autouzupełniania" + }, + "itemSuggestions": { + "message": "Sugerowane elementy" }, "autofillSuggestionsTip": { "message": "Zapisz element logowania dla tej witryny, aby automatycznie wypełnić" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nazwa elementu" }, - "cannotRemoveViewOnlyCollections": { - "message": "Nie można usunąć kolekcji z uprawnieniami tylko do przeglądania: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organizacja jest wyłączona" }, @@ -4369,7 +4358,7 @@ "message": "Dane" }, "passkeys": { - "message": "Passkeys", + "message": "Klucze dostępu", "description": "A section header for a list of passkeys." }, "passwords": { @@ -4377,7 +4366,7 @@ "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Zaloguj się za pomocą passkey", + "message": "Zaloguj się za pomocą klucza dostępu", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4459,7 +4448,7 @@ } }, "reorderToggleButton": { - "message": "Zmień kolejność $LABEL$. Użyj klawiszy że strzałkami aby przenieść element w górę lub w dół.", + "message": "Zmień kolejność $LABEL$. Użyj klawiszy ze strzałkami, aby przenieść element w górę lub w dół.", "placeholders": { "label": { "content": "$1", @@ -4575,22 +4564,16 @@ "message": "Lokalizacja elementu" }, "fileSend": { - "message": "File Send" + "message": "Wysyłka pliku" }, "fileSends": { - "message": "File Sends" + "message": "Wysyłki plików" }, "textSend": { - "message": "Text Send" + "message": "Wysyłka tekstu" }, "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!" + "message": "Wysyłki tekstów" }, "accountActions": { "message": "Akcje konta" @@ -4671,22 +4654,22 @@ "message": "Nie masz uprawnień do edycji tego elementu" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ najpierw wymagane jest odblokowanie kodem PIN lub hasłem." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Odblokowanie biometryczne jest obecnie niedostępne." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ aplikacja desktopowa Bitwarden jest zamknięta." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Odblokowanie biometryczne jest niedostępne, ponieważ nie jest włączone dla $EMAIL$ w aplikacji desktopowej Bitwarden.", "placeholders": { "email": { "content": "$1", @@ -4695,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Odblokowanie biometryczne jest obecnie niedostępne z nieznanego powodu." }, "authenticating": { "message": "Uwierzytelnianie" @@ -4721,7 +4704,7 @@ "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Grawis", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { @@ -4745,7 +4728,7 @@ "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "Daszek", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { @@ -4801,7 +4784,7 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Ukośnik wsteczny", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { @@ -4841,7 +4824,7 @@ "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Ukośnik prawy", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { @@ -4860,22 +4843,22 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Ważna informacja" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Skonfiguruj dwustopniowe logowanie" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden wyśle kod na Twój adres e-mail w celu zweryfikowania logowania z nowych urządzeń, począwszy od lutego 2025 r." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Możesz skonfigurować dwustopniowe logowanie jako alternatywny sposób ochrony konta lub zmienić swój adres e-mail, do którego masz dostęp." }, "remindMeLater": { - "message": "Remind me later" + "message": "Przypomnij mi później" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Czy masz pewny dostęp do swojego adresu e-mail, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4884,16 +4867,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Nie, nie mam" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Tak, mam pewny dostęp do mojego adresu e-mail" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Włącz dwustopniowe logowanie" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Zmień adres e-mail konta" }, "extensionWidth": { "message": "Szerokość rozszerzenia" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Bardzo szerokie" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Nie można usunąć kolekcji z uprawnieniami tylko do przeglądania: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Zaktualizuj aplikację na komputer" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Aby używać odblokowywania biometrycznego, zaktualizuj aplikację na komputerze lub wyłącz odblokowywanie odciskiem palca w ustawieniach aplikacji na komputerze." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index e3409b1da522..a383a4503581 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Gerar frase secreta" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Gerar Nova Senha" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar Identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Seu cofre está trancado. Verifique sua identidade para continuar." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Mostrar cartões em páginas com guias." @@ -995,8 +1013,8 @@ "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" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Exibir Identidades na Aba Atual" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Comprar Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Gerador de usuário" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use esta senha" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Site $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Uma notificação foi enviada para seu dispositivo." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Uma notificação foi enviada para o seu dispositivo" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Certifique-se que sua conta esteja desbloqueada e que a frase de identificação corresponda à do outro dispositivo" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Você será notificado assim que a requisição for aprovada" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login iniciado" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Senha mestra comprometida" }, @@ -3425,38 +3452,6 @@ "message": "Alternar colapso", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importar seus dados para o Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteja seus dados do LastPass e importe para o Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salvar como arquivo não criptografado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar para o Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importando...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dados importados com sucesso!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erro ao importar. Verifique o console para detalhes.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Erro de rede encontrado durante a importação.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias do domínio" }, @@ -4008,7 +4003,10 @@ "message": "Chave de acesso removida" }, "autofillSuggestions": { - "message": "Sugestões de autopreenchimento" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Salvar um item de login para este site autopreenchimento" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nome do item" }, - "cannotRemoveViewOnlyCollections": { - "message": "Você não pode remover coleções com permissões de Somente leitura: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "A organização está desativada" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra Grande" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Você não pode remover coleções com permissões de Somente leitura: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 6b3c190f0b53..da0f34a11662 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -62,7 +62,7 @@ "message": "Uma dica da palavra-passe mestra pode ajudá-lo a lembrar-se da sua palavra-passe, caso se esqueça dela." }, "masterPassHintText": { - "message": "Se se esquecer da sua palavra-passe, a dica da palavra-passe pode ser enviada para o seu e-mail. Máximo de $CURRENT$/$MAXIMUM$ caracteres.", + "message": "Se se esquecer da sua palavra-passe, a dica da palavra-passe pode ser enviada para o seu e-mail. Máximo de $CURRENT$/$MAXIMUM$ carateres.", "placeholders": { "current": { "content": "$1", @@ -296,7 +296,7 @@ "message": "Continuar para a loja de extensões do navegador?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Ajude outras pessoas a descobrir se o Bitwarden lhes é adequado. Visite a loja de extensões do seu navegador e deixe uma avaliação agora." + "message": "Ajude outras pessoas a descobrir se o Bitwarden lhes é adequado. Visite a loja de extensões do seu navegador e deixe uma classificação agora." }, "changeMasterPasswordOnWebConfirmation": { "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." @@ -340,7 +340,7 @@ "message": "Gestor de Segredos Bitwarden" }, "continueToSecretsManagerPageDesc": { - "message": "Armazene, gira e partilhe segredos de programador de forma segura com o Gestor de Segredos Bitwarden. Saiba mais no site bitwarden.com." + "message": "Armazene, faça a gestão e partilhe de forma segura os segredos dos programadores com o Gestor de Segredos Bitwarden. Saiba mais no site bitwarden.com." }, "passwordlessDotDev": { "message": "Passwordless.dev" @@ -379,7 +379,7 @@ "message": "Nome da pasta" }, "folderHintText": { - "message": "Aninhe uma pasta adicionando o nome da pasta principal seguido de um \"/\". Exemplo: Redes Sociais/Fóruns" + "message": "Crie uma subpasta adicionando o nome da pasta principal seguido de um \"/\". Exemplo: Redes Sociais/Fóruns" }, "noFoldersAdded": { "message": "Nenhuma pasta adicionada" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Gerar frase de acesso" }, + "passwordGenerated": { + "message": "Palavra-passe gerada" + }, + "passphraseGenerated": { + "message": "Frase de acesso gerada" + }, + "usernameGenerated": { + "message": "Nome de utilizador gerado" + }, + "emailGenerated": { + "message": "E-mail gerado" + }, "regeneratePassword": { "message": "Regenerar palavra-passe" }, @@ -467,7 +479,7 @@ "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caracteres especiais (!@#$%^&*)", + "message": "Carateres especiais (!@#$%^&*)", "description": "deprecated. Use specialCharactersLabel instead." }, "include": { @@ -475,7 +487,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Incluir caracteres em maiúsculas", + "message": "Incluir carateres em maiúsculas", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -483,7 +495,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Incluir caracteres em minúsculas", + "message": "Incluir carateres em minúsculas", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -499,7 +511,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Incluir caracteres especiais", + "message": "Incluir carateres especiais", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -523,10 +535,10 @@ "message": "Mínimo de números" }, "minSpecial": { - "message": "Mínimo de caracteres especiais" + "message": "Mínimo de carateres especiais" }, "avoidAmbiguous": { - "message": "Evitar caracteres ambíguos", + "message": "Evitar carateres ambíguos", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -639,7 +651,7 @@ "message": "Outras opções" }, "rateExtension": { - "message": "Avaliar a extensão" + "message": "Classificar a extensão" }, "browserNotSupportClipboard": { "message": "O seu navegador Web não suporta a cópia fácil da área de transferência. Em vez disso, copie manualmente." @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidade" }, + "weDontRecognizeThisDevice": { + "message": "Não reconhecemos este dispositivo. Introduza o código enviado para o seu e-mail para verificar a sua identidade." + }, + "continueLoggingIn": { + "message": "Continuar a iniciar sessão" + }, "yourVaultIsLocked": { "message": "O seu cofre está bloqueado. Verifique a sua identidade para continuar." }, @@ -763,7 +781,7 @@ "message": "É necessário reescrever a palavra-passe mestra." }, "masterPasswordMinlength": { - "message": "A palavra-passe mestra deve ter pelo menos $VALUE$ caracteres.", + "message": "A palavra-passe mestra deve ter pelo menos $VALUE$ carateres.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Mostrar sempre cartões como sugestões de preenchimento automático na vista do cofre" }, "showCardsCurrentTab": { "message": "Mostrar cartões na página Separador" @@ -995,8 +1013,8 @@ "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" + "showIdentitiesInVaultViewV2": { + "message": "Mostrar sempre identidades como sugestões de preenchimento automático na vista do cofre" }, "showIdentitiesCurrentTab": { "message": "Mostrar identidades na página Separador" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Adquirir Premium" }, - "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 Bitwarden." }, @@ -1583,7 +1598,7 @@ "message": "Booleano" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Caixa de verificação" }, "cfTypeLinked": { "message": "Associado", @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Gerador de nomes de utilizador" }, + "useThisEmail": { + "message": "Utilizar este e-mail" + }, "useThisPassword": { "message": "Utilizar esta palavra-passe" }, @@ -2168,16 +2186,16 @@ } }, "policyInEffectUppercase": { - "message": "Contém um ou mais caracteres em maiúsculas" + "message": "Contém um ou mais carateres em maiúsculas" }, "policyInEffectLowercase": { - "message": "Contém um ou mais caracteres em minúsculas" + "message": "Contém um ou mais carateres em minúsculas" }, "policyInEffectNumbers": { "message": "Contém um ou mais números" }, "policyInEffectSpecial": { - "message": "Contém um ou mais dos seguintes caracteres especiais $CHARS$", + "message": "Contém um ou mais dos seguintes carateres especiais $CHARS$", "placeholders": { "chars": { "content": "$1", @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "O preenchimento automático e outras funcionalidades relacionadas não serão disponibilizados para estes sites. É necessário atualizar a página para que as alterações tenham efeito." }, - "autofillBlockedNotice": { - "message": "O preenchimento automático está bloqueado para este site. Reveja ou altere esta opção nas definições." + "autofillBlockedNoticeV2": { + "message": "O preenchimento automático está bloqueado para este site." }, - "autofillBlockedTooltip": { - "message": "O preenchimento automático está bloqueado neste site. Reveja nas definições." + "autofillBlockedNoticeGuidance": { + "message": "Alterar esta opção nas definições" }, "websiteItemLabel": { "message": "Site $number$ (URI)", @@ -2772,7 +2790,7 @@ "message": "Saiu da organização." }, "toggleCharacterCount": { - "message": "Mostrar/ocultar contagem de caracteres" + "message": "Mostrar/ocultar contagem de carateres" }, "sessionTimeout": { "message": "A sua sessão expirou. Por favor, volte atrás e tente iniciar sessão novamente." @@ -2839,7 +2857,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Utilize $RECOMMENDED$ caracteres ou mais para gerar uma palavra-passe forte.", + "message": " Utilize $RECOMMENDED$ carateres ou mais para gerar uma palavra-passe forte.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Foi enviada uma notificação para o seu dispositivo." }, + "notificationSentDevicePart1": { + "message": "Desbloqueie o Bitwarden no seu dispositivo ou no " + }, + "notificationSentDeviceAnchor": { + "message": "aplicação web" + }, + "notificationSentDevicePart2": { + "message": "Certifique-se de que a frase da impressão digital corresponde à frase abaixo indicada antes de a aprovar." + }, "aNotificationWasSentToYourDevice": { "message": "Foi enviada uma notificação para o seu dispositivo" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Certifique-se de que a sua conta está desbloqueada e que a frase de impressão digital corresponde à do outro dispositivo" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Será notificado quando o pedido for aprovado" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "A preparar o início de sessão" }, + "logInRequestSent": { + "message": "Pedido enviado" + }, "exposedMasterPassword": { "message": "Palavra-passe mestra exposta" }, @@ -3142,7 +3169,7 @@ "message": "A sua palavra-passe mestra não pode ser recuperada se a esquecer!" }, "characterMinimum": { - "message": "$LENGTH$ caracteres no mínimo", + "message": "$LENGTH$ carateres no mínimo", "placeholders": { "length": { "content": "$1", @@ -3190,7 +3217,7 @@ "message": "O atalho de preenchimento automático de credenciais não está definido. 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.", + "message": "O atalho de preenchimento automático de credenciais é $COMMAND$. Organize todos os atalhos nas definições do navegador.", "placeholders": { "command": { "content": "$1", @@ -3319,7 +3346,7 @@ "message": "Procurar" }, "inputMinLength": { - "message": "O campo deve ter pelo menos $COUNT$ caracteres.", + "message": "O campo deve ter pelo menos $COUNT$ carateres.", "placeholders": { "count": { "content": "$1", @@ -3328,7 +3355,7 @@ } }, "inputMaxLength": { - "message": "O campo não pode exceder os $COUNT$ caracteres de comprimento.", + "message": "O campo não pode exceder os $COUNT$ carateres de comprimento.", "placeholders": { "count": { "content": "$1", @@ -3337,7 +3364,7 @@ } }, "inputForbiddenCharacters": { - "message": "Não são permitidos os seguintes caracteres: $CHARACTERS$", + "message": "Não são permitidos os seguintes carateres: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -3346,7 +3373,7 @@ } }, "inputMinValue": { - "message": "O valor do campo tem de ser, pelo menos, $MIN$ caracteres.", + "message": "O valor do campo tem de ser, pelo menos, $MIN$ carateres.", "placeholders": { "min": { "content": "$1", @@ -3355,7 +3382,7 @@ } }, "inputMaxValue": { - "message": "O valor do campo não pode exceder os $MAX$ caracteres.", + "message": "O valor do campo não pode exceder os $MAX$ carateres.", "placeholders": { "max": { "content": "$1", @@ -3425,38 +3452,6 @@ "message": "Alternar colapso", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importar os seus dados para o Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteger os seus dados LastPass e importar para o Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guardar como ficheiro não encriptado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar para o Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "A importar...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dados importados com sucesso!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erro de importação. Verifique a consola para obter detalhes.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Erro de rede encontrado durante a importação.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias de domínio" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Sugestões de preenchimento automático" }, + "itemSuggestions": { + "message": "Itens sugeridos" + }, "autofillSuggestionsTip": { "message": "Guarde uma credencial deste site para preenchimento automático" }, @@ -4107,7 +4105,7 @@ "message": "Notificações" }, "appearance": { - "message": "Aparência" + "message": "Aspeto" }, "errorAssigningTargetCollection": { "message": "Erro ao atribuir a coleção de destino." @@ -4157,15 +4155,6 @@ "itemName": { "message": "Nome do item" }, - "cannotRemoveViewOnlyCollections": { - "message": "Não é possível remover coleções com permissões de Apenas visualização: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "A organização está desativada" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4644,10 +4627,10 @@ "message": "Ficheiro guardado no dispositivo. Gira-o a partir das transferências do seu dispositivo." }, "showCharacterCount": { - "message": "Mostrar contagem de caracteres" + "message": "Mostrar contagem de carateres" }, "hideCharacterCount": { - "message": "Ocultar contagem de caracteres" + "message": "Ocultar contagem de carateres" }, "itemsInTrash": { "message": "Itens no lixo" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Muito ampla" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Não é possível remover coleções com permissões de Apenas visualização: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Por favor, atualize a sua aplicação para computador" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Para utilizar o desbloqueio biométrico, atualize a sua aplicação para computador ou desative o desbloqueio por impressão digital nas definições dessa mesma app." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 114c01aff444..32cb0215916e 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerare parolă" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificare identitate" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Seiful dvs. este blocat. Verificați-vă identitatea pentru a continua." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Afișați cardurile pe pagina Filă" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Listați elementele cardului pe pagina Filă pentru a facilita completarea automată." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Afișați identitățile pe pagina Filă" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Achiziționare abonament Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "O notificare a fost trimisă pe dispozitivul dvs." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Conectare inițiată" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Parolă principală compromisă" }, @@ -3425,38 +3452,6 @@ "message": "Comutare restrângere", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 53f31813ac5b..8d390e833329 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Создать парольную фразу" }, + "passwordGenerated": { + "message": "Пароль создан" + }, + "passphraseGenerated": { + "message": "Парольная фраза создана" + }, + "usernameGenerated": { + "message": "Имя пользователя создано" + }, + "emailGenerated": { + "message": "Email создан" + }, "regeneratePassword": { "message": "Создать новый пароль" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Подтвердить личность" }, + "weDontRecognizeThisDevice": { + "message": "Мы не распознали это устройство. Введите код, отправленный на ваш email, чтобы подтвердить вашу личность." + }, + "continueLoggingIn": { + "message": "Продолжить вход" + }, "yourVaultIsLocked": { "message": "Ваше хранилище заблокировано. Подтвердите свою личность, чтобы продолжить" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Запрос на добавление элемента, если он отсутствует в вашем хранилище. Применяется ко всем авторизованным аккаунтам." }, - "showCardsInVaultView": { - "message": "Показывать карты как предложение автозаполнения при просмотре Хранилище" + "showCardsInVaultViewV2": { + "message": "Всегда показывать карты как предложения автозаполнения при просмотре хранилища" }, "showCardsCurrentTab": { "message": "Показывать карты на вкладке" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Карты будут отображены на вкладке для удобного автозаполнения." }, - "showIdentitiesInVaultView": { - "message": "Показывать личности как предложение автозаполнения при просмотре Хранилище" + "showIdentitiesInVaultViewV2": { + "message": "Всегда показывать личности как предложения автозаполнения при просмотре хранилища" }, "showIdentitiesCurrentTab": { "message": "Показывать Личности на вкладке" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Купить Премиум" }, - "premiumPurchaseAlert": { - "message": "Вы можете купить Премиум на bitwarden.com. Перейти на сайт сейчас?" - }, "premiumPurchaseAlertV2": { "message": "Премиум можно приобрести в настройках аккаунта в веб-версии Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератор имени пользователя" }, + "useThisEmail": { + "message": "Использовать этот email" + }, "useThisPassword": { "message": "Использовать этот пароль" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Автозаполнение и другие связанные с ним функции не будут предлагаться для этих сайтов. Чтобы изменения вступили в силу, необходимо обновить страницу." }, - "autofillBlockedNotice": { - "message": "Автозаполнение для этого сайта заблокировано. Просмотрите или измените это в настройках." + "autofillBlockedNoticeV2": { + "message": "Автозаполнение для этого сайта заблокировано." }, - "autofillBlockedTooltip": { - "message": "Автозаполнение на этом сайте заблокировано. Просмотрите в настройках." + "autofillBlockedNoticeGuidance": { + "message": "Измените это в настройках" }, "websiteItemLabel": { "message": "Сайт $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "На ваше устройство отправлено уведомление." }, + "notificationSentDevicePart1": { + "message": "Разблокируйте Bitwarden на своем устройстве или" + }, + "notificationSentDeviceAnchor": { + "message": "веб-приложении" + }, + "notificationSentDevicePart2": { + "message": "Перед одобрением убедитесь, что фраза отпечатка совпадает с приведенной ниже." + }, "aNotificationWasSentToYourDevice": { "message": "На ваше устройство было отправлено уведомление" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Убедитесь, что ваш аккаунт разблокирован и фраза отпечатка совпадает с фразой на другом устройстве" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Вы получите уведомление, когда запрос будет одобрен" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Вход инициирован" }, + "logInRequestSent": { + "message": "Запрос отправлен" + }, "exposedMasterPassword": { "message": "Мастер-пароль скомпрометирован" }, @@ -3425,38 +3452,6 @@ "message": "Свернуть/развернуть", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Импортировать данные в Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Защитить данные LastPass и импортировать их в Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Сохранить как незашифрованный файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Импортировать в Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Импорт...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Данные успешно импортированы!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Ошибка импорта. Проверьте консоль для получения подробной информации.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Во время импорта возникла сетевая ошибка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдоним домена" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Предложения по автозаполнению" }, + "itemSuggestions": { + "message": "Предлагаемые элементы" + }, "autofillSuggestionsTip": { "message": "Сохранить логин для этого сайта для автозаполнения" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Название элемента" }, - "cannotRemoveViewOnlyCollections": { - "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Организация деактивирована" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Текстовая Send" }, - "bitwardenNewLook": { - "message": "У Bitwarden новый облик!" - }, - "bitwardenNewLookDesc": { - "message": "Теперь автозаполнение и поиск на вкладке Хранилище стали проще и интуитивно понятнее, чем когда-либо. Осмотритесь!" - }, "accountActions": { "message": "Действия аккаунта" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Очень широкое" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Пожалуйста, обновите приложение для компьютера" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Чтобы использовать биометрическую разблокировку, обновите приложение для компьютера или отключите разблокировку по отпечатку пальца в настройках компьютера." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 56cf378344fb..bda76601e902 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "මුරපදය ප්රතිජනනය" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "අනන්යතාවය සත්යාපනය කරන්න" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ඔබේ සුරක්ෂිතාගාරය අගුළු දමා ඇත. දිගටම කරගෙන යාමට ඔබේ අනන්යතාවය සත්යාපනය කරන්න." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "වාරික මිලදී" }, - "premiumPurchaseAlert": { - "message": "ඔබට bitwarden.com වෙබ් සුරක්ෂිතාගාරයේ වාරික සාමාජිකත්වය මිලදී ගත හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 08bfcc79f6aa..f2b50505e8a7 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generovať prístupovú frázu" }, + "passwordGenerated": { + "message": "Heslo vygenerované" + }, + "passphraseGenerated": { + "message": "Prístupová fráza vygenerovaná" + }, + "usernameGenerated": { + "message": "Používateľské meno vygenerované" + }, + "emailGenerated": { + "message": "E-mail vygenoravný" + }, "regeneratePassword": { "message": "Vygenerovať nové heslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Overiť identitu" }, + "weDontRecognizeThisDevice": { + "message": "Toto zariadenie nepoznáme. Na overenie vašej totožnosti zadajte kód, ktorý bol zaslaný na váš e-mail." + }, + "continueLoggingIn": { + "message": "Pokračovať v prihlasovaní" + }, "yourVaultIsLocked": { "message": "Váš trezor je uzamknutý. Ak chcete pokračovať, overte svoju identitu." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Vždy zobraziť karty ako návrhy automatického vypĺňania v zobrazení trezora" }, "showCardsCurrentTab": { "message": "Zobraziť karty na stránke \"Aktuálna karta\"" @@ -995,8 +1013,8 @@ "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" + "showIdentitiesInVaultViewV2": { + "message": "Vždy zobraziť identity ako návrhy automatického vypĺňania v zobrazení trezora" }, "showIdentitiesCurrentTab": { "message": "Zobraziť identity na stránke \"Aktuálna karta\"" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Zakúpiť Prémiový účet" }, - "premiumPurchaseAlert": { - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generátor používateľského mena" }, + "useThisEmail": { + "message": "Použiť tento e-mail" + }, "useThisPassword": { "message": "Použiť toto heslo" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Automatické vypĺňanie a ďalšie súvisiace funkcie sa na týchto webových stránkach nebudú ponúkať. Aby sa zmeny prejavili, musíte stránku obnoviť." }, - "autofillBlockedNotice": { - "message": "Automatické vypĺňanie je pre túto webovú stránku zablokované. Skontrolujte alebo zmeňte to v nastaveniach." + "autofillBlockedNoticeV2": { + "message": "Automatické vypĺňanie je pre túto webovú stránku zablokované." }, - "autofillBlockedTooltip": { - "message": "Automatické vypĺňanie je na tejto webovej stránke zablokované. Pozrite v nastaveniach." + "autofillBlockedNoticeGuidance": { + "message": "Zmeňte to v nastaveniach" }, "websiteItemLabel": { "message": "Webstránka $number$ (URI)", @@ -2884,7 +2902,7 @@ "message": "Služba" }, "forwardedEmail": { - "message": "Alias preposlaného e-mailu" + "message": "Alias presmerovaného e-mailu" }, "forwardedEmailDesc": { "message": "Vytvoriť e-mailový alias pomocou externej služby preposielania." @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Do vášho zariadenia bolo odoslané upozornenie." }, + "notificationSentDevicePart1": { + "message": "Odomknúť Bitwarden vo svojom zariadení alebo vo" + }, + "notificationSentDeviceAnchor": { + "message": "webovej aplikácii" + }, + "notificationSentDevicePart2": { + "message": "Pred schválením sa uistite, že sa odtlačok prístupovej frázy zhoduje s tou uvedenou nižšie." + }, "aNotificationWasSentToYourDevice": { "message": "Do vášho zariadenia bolo odoslané upozornenie" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Uistite sa, že je váš účet odomknutý a fráza odtlačku prsta sa zhoduje s frázou na druhom zariadení" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Po schválení žiadosti budete informovaní" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Iniciované prihlásenie" }, + "logInRequestSent": { + "message": "Požiadavka bola odoslaná" + }, "exposedMasterPassword": { "message": "Odhalené hlavné heslo" }, @@ -3425,38 +3452,6 @@ "message": "Prepnúť zbalenie", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importovať údaje do Bitwardenu?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Ochrániť údaje z LastPassu a importovať ich do Bitwardenu?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Uložiť ako nezašifrovaný súbor", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importovať do Bitwardenu", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importovanie...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Import údajov prebehol úspešne!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Chyba pri importovaní. Podrobnosti nájdete v konzole.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Počas importu sa vyskytla chyba siete.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias doména" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Návrhy automatického vypĺňania" }, + "itemSuggestions": { + "message": "Navrhované položky" + }, "autofillSuggestionsTip": { "message": "Uložte položku prihlásenia pre tento web na automatické vyplnenie" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Názov položky" }, - "cannotRemoveViewOnlyCollections": { - "message": "Zbierky, ktoré môžete len zobraziť nemôžete odstrániť: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organizácia je vypnutá" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4875,7 +4858,7 @@ "message": "Pripomenúť neskôr" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Máte spoľahlivý prístup k svojmu e-mailu, $EMAIL$?", + "message": "Máte zaručený prístup k e-mailu $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4887,7 +4870,7 @@ "message": "Nie, nemám" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Áno, mám spoľahlivý prístup k svojmu e-mailu" + "message": "Áno, mám zaručený prístup k e-mailu" }, "turnOnTwoStepLogin": { "message": "Zapnúť dvojstupňové prihlásenie" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra široké" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Zbierky, ktoré môžete len zobraziť nemôžete odstrániť: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aktualizujte desktopovú aplikáciu" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Ak chcete používať biometrické odomykanie, aktualizujte desktopovú aplikáciu alebo vypnite odomykanie odtlačkom prsta v nastaveniach desktopovej aplikácie." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 2d2ee455415e..cbff6df98c22 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Ponovno ustvari geslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Preverjanje istovetnosti" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Vaš trezor je zaklenjen. Za nadaljevanje potrdite svojo identiteto." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Prikaži kartice na strani Zavihek" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Na strani Zavihek prikaži kartice za lažje samodejno izpoljnjevanje." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete na strani Zavihek" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Kupite premium članstvo" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 01d95a6ed1b0..cb143049ca37 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -159,7 +159,7 @@ "message": "Копирај јавни кључ" }, "copyFingerprint": { - "message": "Копирати отисак" + "message": "Копирај отисак прста" }, "copyCustomField": { "message": "Копирати $FIELD$", @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генеришите приступну фразу" }, + "passwordGenerated": { + "message": "Лозинка генерисана" + }, + "passphraseGenerated": { + "message": "Приступна фраза је генерисана" + }, + "usernameGenerated": { + "message": "Корисничко име генерисано" + }, + "emailGenerated": { + "message": "Имејл генерисан" + }, "regeneratePassword": { "message": "Поново генериши лозинку" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Потврдите идентитет" }, + "weDontRecognizeThisDevice": { + "message": "Не препознајемо овај уређај. Унесите код послат на адресу ваше електронске поште да би сте потврдили ваш идентитет." + }, + "continueLoggingIn": { + "message": "Настави са пријављивањем" + }, "yourVaultIsLocked": { "message": "Сеф је закључан. Унесите главну лозинку за наставак." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Затражите да додате ставку ако она није пронађена у вашем сефу. Односи се на све пријављене налоге." }, - "showCardsInVaultView": { - "message": "Прикажите картице као предлоге за ауто-попуњавање у приказу сефа" + "showCardsInVaultViewV2": { + "message": "Увек приказуј картице као препоруке аутоматског попуњавања на приказу трезора" }, "showCardsCurrentTab": { "message": "Прикажи кредитне картице на страници картице" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Прикажи ставке кредитних картица на страници картице за лакше аутоматско допуњавање." }, - "showIdentitiesInVaultView": { - "message": "Прикажите идентитете као предлоге за ауто-попуњавање у приказу сефа" + "showIdentitiesInVaultViewV2": { + "message": "Увек приказуј идентитете као препоруке аутоматског попуњавања на приказу трезора" }, "showIdentitiesCurrentTab": { "message": "Прикажи идентитете на страници" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Купити премијум" }, - "premiumPurchaseAlert": { - "message": "Можете купити премијум претплату на bitwarden.com. Да ли желите да посетите веб сајт сада?" - }, "premiumPurchaseAlertV2": { "message": "Можете да купите Премиум у подешавањима налога у веб апликацији Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератор корисничког имена" }, + "useThisEmail": { + "message": "Користи ову епошту" + }, "useThisPassword": { "message": "Употреби ову лозинку" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Блокирани домени" }, "excludedDomains": { "message": "Изузети домени" @@ -2337,13 +2355,13 @@ "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене за све пријављене налоге. Морате освежити страницу да би промене ступиле на снагу." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Аутоматско попуњавање и сродне функције неће бити понуђене за ове веб сајтове. Морате освежити страницу да би се измене примениле." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Аутоматско попуњавање је блокирано за овај веб сајт." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Промените ово у подешавањима" }, "websiteItemLabel": { "message": "Сајт $number$ (УРЛ)", @@ -2364,7 +2382,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Измене блокираних домена су сачуване" }, "excludedDomainsSavedSuccess": { "message": "Изузете промене домена су сачуване" @@ -2805,17 +2823,17 @@ "message": "Грешка" }, "decryptionError": { - "message": "Decryption error" + "message": "Грешка при декрипцији" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden није могао да декриптује ставке из трезора наведене испод." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Обратите се корисничкој подршци", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "да бисте избегли додатни губитак података.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Обавештење је послато на ваш уређај." }, + "notificationSentDevicePart1": { + "message": "Откључај Bitwarden на твом уређају или на" + }, + "notificationSentDeviceAnchor": { + "message": "веб апликација" + }, + "notificationSentDevicePart2": { + "message": "Потврдите да се фраза отиска прста поклапа са овом испод пре одобравања." + }, "aNotificationWasSentToYourDevice": { "message": "Обавештење је послато на ваш уређај" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Уверите се да је ваш налог откључан и да се фраза отиска подудара на другом уређају" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Бићете обавештени када захтев буде одобрен" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Пријава је покренута" }, + "logInRequestSent": { + "message": "Захтев је послат" + }, "exposedMasterPassword": { "message": "Изложена главна лозинка" }, @@ -3425,38 +3452,6 @@ "message": "Промени проширење", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Увезите своје податке у Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Заштитите своје LastPass податке и увезите у Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Сачувати као нешифровану датотеку", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Увоз у Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Увоз...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Подаци су успешно увезени!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Грешка при увозу. Проверите конзолу за детаље.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Дошло је до грешке на мрежи током увоза.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Домен алијаса" }, @@ -4008,7 +4003,10 @@ "message": "Приступни кључ је уклоњен" }, "autofillSuggestions": { - "message": "Предлози за ауто-попуњавање" + "message": "Предлози аутоматског попуњавања" + }, + "itemSuggestions": { + "message": "Предложене ставке" }, "autofillSuggestionsTip": { "message": "Сачувајте ставку за пријаву за ову локацију за ауто-попуњавање" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Име ставке" }, - "cannotRemoveViewOnlyCollections": { - "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Организација је деактивирана" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Текст „Send“" }, - "bitwardenNewLook": { - "message": "Bitwarden има нови изглед!" - }, - "bitwardenNewLookDesc": { - "message": "Лакше је и интуитивније него икада да се аутоматски попуњава и тражи са картице Сефа. Проверите!" - }, "accountActions": { "message": "Акције везане за налог" }, @@ -4671,22 +4654,22 @@ "message": "Немате дозволу да уређујете ову ставку" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Биометријско откључавање није доступно јер је пре тога потребно унети ПИН или лозинку за откључавање." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Биометријско откључавање тренутно није доступно." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Биометријско откључавање није доступно због лоше подешених системских датотека." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Биометријско откључавање није доступно због лоше подешених системских датотека." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Биометријско откључавање није доступно јер је Bitwarden апликација на рачунару угашена." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Биометријско откључавање није доступно јер није омогућено за $EMAIL$ у Bitwarden апликацији на рачунару.", "placeholders": { "email": { "content": "$1", @@ -4695,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Биометријско откључавање није доступно из непознатог разлога." }, "authenticating": { "message": "Аутентификација" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Врло широко" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Молим вас надоградите вашу апликацију на рачунару" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Да би сте користили биометријско откључавање, надоградите вашу апликацију на рачунару, или онемогућите откључавање отиском прста у подешавањима на рачунару." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index a443a8e6b2e1..727326e1cd66 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generera lösenfras" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Återskapa lösenord" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifiera identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Ditt valv är låst. Verifiera din identitet för att fortsätta." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Visa kort på fliksida" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Lista kortobjekt på fliksidan för enkel automatisk fyllning." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Visa identiteter på fliksidan" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Köp Premium" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Webbplats $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "En avisering har skickats till din enhet." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Inloggning påbörjad" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Huvudlösenordet har exponerats" }, @@ -3425,38 +3452,6 @@ "message": "Växla synlig/dold", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importera din data till Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Skydda din LastPass-data och importera till Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Spara som okrypterad fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importera till Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerar...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data har importerats till ditt valv!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fel vid importeringen. Kolla konsolen för detaljer.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Nätverksfel uppstod vid import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomän" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Objektnamn" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index e34751eea7dd..800523bdc2ec 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 1b493de3d2c9..04775fa0f915 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "ยืนยันตัวตน" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ตู้เซฟของคุณถูกล็อก ยืนยันตัวตนของคุณเพื่อดำเนินการต่อ" }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "แสดงการ์ดบนหน้าแท็บ" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "บัตรรายการในหน้าแท็บเพื่อให้ป้อนอัตโนมัติได้ง่าย" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "แสดงตัวตนบนหน้าแท็บ" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -3425,38 +3452,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 8095a9f6045c..b6d54e96ecf5 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -385,7 +385,7 @@ "message": "Hiç klasör eklenmedi" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "Kasanızdaki kayıtları organize etmek için klasörler oluşturun" }, "deleteFolderPermanently": { "message": "Bu klasörü kalıcı olarak silmek istediğinizden emin misiniz?" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Parola üret" }, + "passwordGenerated": { + "message": "Parola üretildi" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Kullanıcı adı üretildi" + }, + "emailGenerated": { + "message": "E-posta üretildi" + }, "regeneratePassword": { "message": "Yeni parola oluştur" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Kimliği doğrula" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanıyamadık. Kimliğinizi doğrulamak için e-postanıza gönderilen kodu girin." + }, + "continueLoggingIn": { + "message": "Giriş yapmaya devam et" + }, "yourVaultIsLocked": { "message": "Kasanız kilitli. Devam etmek için kimliğinizi doğrulayın." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Kasa görünümünde kartları her zaman otomatik doldurma önerisi olarak göster" }, "showCardsCurrentTab": { "message": "Sekme sayfasında kartları göster" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kolay otomatik doldurma için sekme sayfasında kartları listele." }, - "showIdentitiesInVaultView": { - "message": "Kasa görünümünde kimlikleri otomatik doldurma önerisi olarak göster" + "showIdentitiesInVaultViewV2": { + "message": "Kasa görünümünde kimlikleri her zaman otomatik doldurma önerisi olarak göster" }, "showIdentitiesCurrentTab": { "message": "Sekme sayfasında kimlikleri göster" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium satın al" }, - "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." }, @@ -1275,7 +1290,7 @@ "message": "Bitwarden'ı desteklediğiniz için teşekkür ederiz." }, "premiumFeatures": { - "message": "Premium'a yükseltin ve şunları alın:" + "message": "Premium'a geçmenin avantajları:" }, "premiumPrice": { "message": "Bunların hepsi sadece yılda $PRICE$!", @@ -1466,7 +1481,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Önerileri otomatik doldur" + "message": "Otomatik doldurma önerileri" }, "showInlineMenuLabel": { "message": "Form alanlarında otomatik doldurma önerilerini göster" @@ -1502,13 +1517,13 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "enableAutoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "enableAutoFillOnPageLoadDesc": { - "message": "Sayfa yüklendiğinde giriş formu tespit edilirse otomatik olarak formu doldur." + "message": "Sayfa yüklenince giriş formu tespit edilirse otomatik olarak formu doldur." }, "experimentalFeature": { "message": "Ele geçirilmiş veya güvenilmeyen web siteleri sayfa yüklenirken otomatik doldurmayı suistimal edebilir." @@ -1523,19 +1538,19 @@ "message": "Hesaplar için varsayılan otomatik doldurma ayarı" }, "defaultAutoFillOnPageLoadDesc": { - "message": "\"Sayfa yüklendiğinde otomatik doldur\"u her hesabın \"Düzenle\" görünümünden ayrı ayrı kapatabilirsiniz." + "message": "\"Sayfa yüklenince otomatik doldur\"u her hesabın \"Düzenle\" görünümünden ayrı ayrı kapatabilirsiniz." }, "itemAutoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur (Seçeneklerde ayarlanmışsa)" + "message": "Sayfa yüklenince otomatik doldur (Seçeneklerde ayarlanmışsa)" }, "autoFillOnPageLoadUseDefault": { "message": "Varsayılan ayarı kullan" }, "autoFillOnPageLoadYes": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "autoFillOnPageLoadNo": { - "message": "Sayfa yüklendiğinde otomatik doldurma" + "message": "Sayfa yüklenince otomatik doldurma" }, "commandOpenPopup": { "message": "Kasayı açılır pencerede aç" @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Kullanıcı adı üreteci" }, + "useThisEmail": { + "message": "Bu e-postayı kullan" + }, "useThisPassword": { "message": "Bu parolayı kullan" }, @@ -2337,13 +2355,13 @@ "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." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Bu siteler için otomatik doldurma ve diğer ilgili özellikler önerilmeyecektir. Değişikliklerin devreye girmesi için sayfayı yenilemelisiniz." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Bu sitede otomatik doldurma engellenmiş." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Bunu ayarlardan değiştirebilirsiniz" }, "websiteItemLabel": { "message": "Web sitesi $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Cihazınıza bir bildirim gönderildi." }, + "notificationSentDevicePart1": { + "message": "Bitwarden kilidini cihazınızdan veya" + }, + "notificationSentDeviceAnchor": { + "message": "web uygulamasından açın" + }, + "notificationSentDevicePart2": { + "message": "Onay vermeden önce parmak izi ifadesinin aşağıdakiyle eşleştiğini kontrol edin." + }, "aNotificationWasSentToYourDevice": { "message": "Cihazınıza bir bildirim gönderildi" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Lütfen hesabınızın kilidinin açık olduğundan ve parmak izi ifadesinin diğer cihazla eşleştiğinden emin olun" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "İsteğiniz onaylanınca size haber vereceğiz" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Giriş başlatıldı" }, + "logInRequestSent": { + "message": "İstek gönderildi" + }, "exposedMasterPassword": { "message": "Açığa Çıkmış Ana Parola" }, @@ -3151,7 +3178,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Kuruluş ilkeleriniz, sayfa yüklendiğinde otomatik doldurmayı etkinleştirdi." + "message": "Kuruluş ilkeleriniz, sayfa yüklenince otomatik doldurmayı etkinleştirdi." }, "howToAutofill": { "message": "Otomatik doldurma nasıl yapılır?" @@ -3425,47 +3452,15 @@ "message": "Daraltmayı aç/kapat", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Verileriniz Bitwarden'a aktarılsın mı?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass verileriniz korunsun ve Bitwarden'a aktarılsın mı?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Şifrelenmemiş dosya olarak kaydet", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden'a aktar", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "İçe aktarılıyor...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Veriler başarıyla içe aktarıldı!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "İçe aktarma hatası. Ayrıntılar için konsolu kontrol edin.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "İçe aktarma sırasında ağ hatasıyla karşılaşıldı.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias alan adı" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Ana parolayı yeniden isteyen kayıtlar sayfa yüklendiğinde otomatik olarak doldurulamaz. Sayfa yüklendiğinde otomatik doldurma kapatıldı.", + "message": "Ana parolayı yeniden isteyen kayıtlar sayfa yüklenince otomatik olarak doldurulamaz. Sayfa yüklenince otomatik doldurma kapatıldı.", "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ı.", + "message": "Sayfa yüklenince otomatik doldurma, varsayılan ayarı kullanacak şekilde ayarlandı.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { @@ -3740,7 +3735,7 @@ "message": "Geçiş anahtarı" }, "accessing": { - "message": "Erişim" + "message": "Erişilen konum:" }, "loggedInExclamation": { "message": "Giriş yapıldı!" @@ -4008,7 +4003,10 @@ "message": "Geçiş anahtarı kaldırıldı" }, "autofillSuggestions": { - "message": "Önerileri otomatik doldur" + "message": "Otomatik doldurma önerileri" + }, + "itemSuggestions": { + "message": "Önerilen kayıtlar" }, "autofillSuggestionsTip": { "message": "Otomatik doldurma için bu siteye ait bir hesap kaydededin" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Kayıt adı" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Kuruluş pasifleştirilmiş" }, @@ -4333,7 +4322,7 @@ } }, "autoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "cardExpiredTitle": { "message": "Kartın süresi dolmuş" @@ -4586,12 +4575,6 @@ "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": "Hesap işlemleri" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Ekstra geniş" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lütfen masaüstü uygulamanızı güncelleyin" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index d6b0b88ead25..9c9041dd008d 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генерувати парольну фразу" }, + "passwordGenerated": { + "message": "Пароль згенеровано" + }, + "passphraseGenerated": { + "message": "Парольну фразу згенеровано" + }, + "usernameGenerated": { + "message": "Ім'я користувача згенеровано" + }, + "emailGenerated": { + "message": "Адресу е-пошти згенеровано" + }, "regeneratePassword": { "message": "Генерувати новий" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Виконати перевірку" }, + "weDontRecognizeThisDevice": { + "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." + }, + "continueLoggingIn": { + "message": "Продовжити вхід" + }, "yourVaultIsLocked": { "message": "Ваше сховище заблоковане. Для продовження виконайте перевірку." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Запитувати про додавання запису, якщо такого не знайдено у вашому сховищі. Застосовується для всіх облікових записів, до яких виконано вхід." }, - "showCardsInVaultView": { - "message": "Показувати картки як пропозиції автозаповнення в режимі перегляду сховища" + "showCardsInVaultViewV2": { + "message": "Завжди показувати картки як пропозиції автозаповнення в режимі перегляду сховища" }, "showCardsCurrentTab": { "message": "Показувати картки на вкладці" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Показувати список карток на сторінці вкладки для легкого автозаповнення." }, - "showIdentitiesInVaultView": { - "message": "Показувати посвідчення як пропозиції автозаповнення в режимі перегляду сховища" + "showIdentitiesInVaultViewV2": { + "message": "Завжди показувати посвідчення як пропозиції автозаповнення в режимі перегляду сховища" }, "showIdentitiesCurrentTab": { "message": "Показувати посвідчення на вкладці" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Придбати преміум" }, - "premiumPurchaseAlert": { - "message": "Ви можете передплатити преміум у сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" - }, "premiumPurchaseAlertV2": { "message": "Ви можете придбати Преміум у налаштуваннях облікового запису вебпрограмі Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератор імені користувача" }, + "useThisEmail": { + "message": "Використати цю е-пошту" + }, "useThisPassword": { "message": "Використати цей пароль" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Автозаповнення та інші пов'язані функції не пропонуватимуться для цих вебсайтів. Вам слід оновити сторінку для застосування змін." }, - "autofillBlockedNotice": { - "message": "Автозаповнення заблоковано для цього вебсайту. Перегляньте або змініть це в налаштуваннях." + "autofillBlockedNoticeV2": { + "message": "Автозаповнення для цього вебсайту заблоковано." }, - "autofillBlockedTooltip": { - "message": "Автозаповнення заблоковано на цьому вебсайті. Перевірте налаштування." + "autofillBlockedNoticeGuidance": { + "message": "Змінити в налаштуваннях" }, "websiteItemLabel": { "message": "Вебсайт $number$ (URI)", @@ -2391,7 +2409,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Надіслати подробиці", + "message": "Подробиці відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -2805,17 +2823,17 @@ "message": "Помилка" }, "decryptionError": { - "message": "Decryption error" + "message": "Помилка розшифрування" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden не зміг розшифрувати вказані нижче елементи сховища." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Зверніться до служби підтримки клієнтів,", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "щоб уникнути втрати даних.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Сповіщення було надіслано на ваш пристрій." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Сповіщення надіслано на ваш пристрій" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Переконайтеся, що ваш обліковий запис розблоковано і фраза відбитка на іншому пристрої збігається" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Після схвалення запиту ви отримаєте сповіщення" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Ініційовано вхід" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Головний пароль викрито" }, @@ -3425,38 +3452,6 @@ "message": "Згорнути/розгорнути", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Імпортувати ваші дані до Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Захистити ваші дані LastPass та імпортувати до Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Зберегти незашифрований файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Імпортувати до Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Триває імпортування...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Дані успішно імпортовано!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Помилка імпортування. Перевірте консоль для перегляду подробиць.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Під час імпортування сталася мережева помилка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдонім домену" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "Пропозиції автозаповнення" }, + "itemSuggestions": { + "message": "Запропоновані записи" + }, "autofillSuggestionsTip": { "message": "Зберегти дані входу цього сайту для автозаповнення" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "Назва запису" }, - "cannotRemoveViewOnlyCollections": { - "message": "Ви не можете вилучати збірки, маючи дозвіл лише на перегляд: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Організацію деактивовано" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "Відправлення тексту" }, - "bitwardenNewLook": { - "message": "Bitwarden має новий вигляд!" - }, - "bitwardenNewLookDesc": { - "message": "Ще простіше автозаповнення та інтуїтивніший пошук у сховищі. Ознайомтеся!" - }, "accountActions": { "message": "Дії з обліковим записом" }, @@ -4671,22 +4654,22 @@ "message": "Вам не дозволено редагувати цей запис" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Біометричне розблокування недоступне, оскільки спочатку потрібно ввести PIN-код або пароль." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Біометричне розблокування наразі недоступне." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Біометричне розблокування недоступне, оскільки програму Bitwarden для комп'ютера закрито." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Біометричне розблокування недоступне, оскільки воно не увімкнене для $EMAIL$ у програмі Bitwarden для комп'ютера.", "placeholders": { "email": { "content": "$1", @@ -4695,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Біометричне розблокування зараз недоступне з невідомої причини." }, "authenticating": { "message": "Аутентифікація" @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Дуже широке" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Ви не можете вилучати збірки, маючи дозвіл лише на перегляд: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Оновіть свою комп'ютерну програму" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Щоб використовувати біометричне розблокування, оновіть комп'ютерну програму, або вимкніть розблокування відбитком пальця в налаштуваннях системи." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 4ccdaf808f33..b398b43bab36 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Tạo lại mật khẩu" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Xác minh danh tính" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Kho của bạn đã bị khóa. Xác minh danh tính của bạn để mở khoá." }, @@ -986,8 +1004,8 @@ "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" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" @@ -995,8 +1013,8 @@ "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" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Hiển thị danh tính trên trang Tab" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Mua bản Cao Cấp" }, - "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." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Bộ tạo tên người dùng" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Trang Web $number$ (URI)", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "Một thông báo đã được gửi đến thiết bị của bạn." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Bắt đầu đăng nhập" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Mật khẩu chính bị lộ" }, @@ -3425,38 +3452,6 @@ "message": "Bật/tắt thu gọn", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Nhập dữ liệu của bạn vào Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "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": "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": "Nhập vào Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Đang nhập...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dữ liệu đã được nhập thành công!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "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": "Đã 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": "Tên miền thay thế" }, @@ -4008,7 +4003,10 @@ "message": "Đã xóa mã khoá" }, "autofillSuggestions": { - "message": "Gợi ý điền tự động" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Lưu thông tin đăng nhập cho trang này để tự động điền" @@ -4157,15 +4155,6 @@ "itemName": { "message": "Tên mục" }, - "cannotRemoveViewOnlyCollections": { - "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", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Tổ chức không còn hoạt động" }, @@ -4586,12 +4575,6 @@ "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" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "Extra wide" + }, + "cannotRemoveViewOnlyCollections": { + "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", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index dd6a2286c4ab..78783b637297 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -56,7 +56,7 @@ "message": "主密码" }, "masterPassDesc": { - "message": "主密码是您访问密码库的唯一密码。它非常重要,请您不要忘记。一旦忘记,无任何办法恢复此密码。" + "message": "主密码是用于访问您的密码库的密码。不要忘记您的主密码,这一点非常重要。一旦忘记,无任何办法恢复此密码。" }, "masterPassHintDesc": { "message": "主密码提示可以在您忘记密码时帮您回忆起来。" @@ -379,7 +379,7 @@ "message": "文件夹名称" }, "folderHintText": { - "message": "通过在父文件夹名后面跟随「/」来嵌套文件夹。示例:Social/Forums" + "message": "通过在父文件夹名后面添加「/」来嵌套文件夹。示例:Social/Forums" }, "noFoldersAdded": { "message": "未添加文件夹" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "生成密码短语" }, + "passwordGenerated": { + "message": "密码已生成" + }, + "passphraseGenerated": { + "message": "密码短语已生成" + }, + "usernameGenerated": { + "message": "用户名已生成" + }, + "emailGenerated": { + "message": "电子邮箱已生成" + }, "regeneratePassword": { "message": "重新生成密码" }, @@ -579,7 +591,7 @@ "message": "私密备注" }, "note": { - "message": "备注" + "message": "笔记" }, "editItem": { "message": "编辑项目" @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "验证身份" }, + "weDontRecognizeThisDevice": { + "message": "我们无法识别这个设备。请输入发送到您电子邮箱中的代码以验证您的身份。" + }, + "continueLoggingIn": { + "message": "继续登录" + }, "yourVaultIsLocked": { "message": "您的密码库已锁定。请先验证您的身份。" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "如果在密码库中找不到项目,询问添加一个。适用于所有已登录的账户。" }, - "showCardsInVaultView": { - "message": "在密码库视图中将支付卡显示为自动填充建议" + "showCardsInVaultViewV2": { + "message": "在密码库视图中将支付卡始终显示为自动填充建议" }, "showCardsCurrentTab": { "message": "在标签页上显示支付卡" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "在标签页上列出支付卡项目,以便于自动填充。" }, - "showIdentitiesInVaultView": { - "message": "在密码库视图中将身份显示为自动填充建议" + "showIdentitiesInVaultViewV2": { + "message": "在密码库视图中将身份始终显示为自动填充建议" }, "showIdentitiesCurrentTab": { "message": "在标签页上显示身份" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "购买高级版" }, - "premiumPurchaseAlert": { - "message": "您可以在 bitwarden.com 网页版密码库购买高级会员。现在要访问吗?" - }, "premiumPurchaseAlertV2": { "message": "您可以在 Bitwarden 网页 App 的账户设置中购买高级版。" }, @@ -1744,7 +1759,7 @@ "message": "州 / 省" }, "zipPostalCode": { - "message": "邮编 / 邮政代码" + "message": "邮政编码" }, "country": { "message": "国家" @@ -1856,7 +1871,7 @@ "message": "检查密码是否已经被公开。" }, "passwordExposed": { - "message": "此密码在泄露数据中已被公开 $VALUE$ 次。请立即修改。", + "message": "此密码在数据泄露中已被暴露 $VALUE$ 次。请立即修改。", "placeholders": { "value": { "content": "$1", @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "用户名生成器" }, + "useThisEmail": { + "message": "使用此电子邮箱" + }, "useThisPassword": { "message": "使用此密码" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "屏蔽域名" + "message": "屏蔽的域名" }, "excludedDomains": { "message": "排除域名" @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "将不会为这些网站提供自动填充和其他相关功能。您必须刷新页面才能使更改生效。" }, - "autofillBlockedNotice": { - "message": "该网站的自动填充功能已被阻止。请在设置中查看或更改。" + "autofillBlockedNoticeV2": { + "message": "该网站的自动填充已被屏蔽。" }, - "autofillBlockedTooltip": { - "message": "该网站的自动填充功能已被阻止。请在设置中查看。" + "autofillBlockedNoticeGuidance": { + "message": "在设置中更改它" }, "websiteItemLabel": { "message": "网站 $number$ (URI)", @@ -2488,7 +2506,7 @@ "message": "自定义" }, "sendPasswordDescV3": { - "message": "添加一个用于收件人访问此 Send 的可选密码。", + "message": "添加一个用于接收者访问此 Send 的可选密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { @@ -2646,7 +2664,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "$TOTAL$ 不足", + "message": "总计 $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "通知已发送到您的设备。" }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "网页 App" + }, + "notificationSentDevicePart2": { + "message": "在批准前,请确保指纹短语与下面的一致。" + }, "aNotificationWasSentToYourDevice": { "message": "通知已发送到您的设备" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "确保您的账户已解锁,并且指纹短语与其他设备上的相匹配。" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "请求获得批准后,您将收到通知" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "登录已发起" }, + "logInRequestSent": { + "message": "请求已发送" + }, "exposedMasterPassword": { "message": "已暴露的主密码" }, @@ -3425,38 +3452,6 @@ "message": "切换折叠", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "导入您的数据到 Bitwarden 吗?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "保护您的 LastPass 数据并导入到 Bitwarden 吗?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "保存为未加密的文件", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "导入到 Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "正在导入...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "数据成功导入!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "导入时出错。检查控制台以获取详细信息。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "导入过程中遇到网络错误。", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "别名域" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "自动填充建议" }, + "itemSuggestions": { + "message": "建议的项目" + }, "autofillSuggestionsTip": { "message": "将此站点保存为登录项目以用于自动填充" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "项目名称" }, - "cannotRemoveViewOnlyCollections": { - "message": "您无法删除仅具有「查看」权限的集合:$COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "组织已停用" }, @@ -4237,7 +4226,7 @@ "message": "筛选" }, "filterVault": { - "message": "密码库筛选" + "message": "筛选密码库" }, "filterApplied": { "message": "已应用一个筛选" @@ -4586,12 +4575,6 @@ "textSends": { "message": "文本 Send" }, - "bitwardenNewLook": { - "message": "Bitwarden 拥有一个新的外观!" - }, - "bitwardenNewLookDesc": { - "message": "从密码库标签页自动填充和搜索比以往任何时候都更简单直观。来看看吧!" - }, "accountActions": { "message": "账户操作" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "超宽" + }, + "cannotRemoveViewOnlyCollections": { + "message": "您无法删除仅具有「查看」权限的集合:$COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "请更新您的桌面应用程序" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "要使用生物识别解锁,请更新您的桌面应用程序,或在桌面设置中禁用指纹解锁。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 467deffd8150..30351228927c 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -35,7 +35,7 @@ "message": "設定一個強密碼" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "設定密碼以完成創建您的帳戶。" + "message": "設定密碼以完成建立您的帳號" }, "enterpriseSingleSignOn": { "message": "企業單一登入" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "產生密碼短語" }, + "passwordGenerated": { + "message": "已產生密碼" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "重新產生密碼" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "驗證身份" }, + "weDontRecognizeThisDevice": { + "message": "我們無法識別此裝置。請輸入已傳送到您電子郵件的驗證碼以驗證您的身分。" + }, + "continueLoggingIn": { + "message": "繼續登入" + }, "yourVaultIsLocked": { "message": "您的密碼庫已鎖定。請驗證身分以繼續。" }, @@ -745,7 +763,7 @@ "message": "若您忘記主密碼,將會無法找回!" }, "masterPassHintLabel": { - "message": "您已成功創建新帳戶!" + "message": "主密碼提示" }, "errorOccurred": { "message": "發生錯誤" @@ -779,7 +797,7 @@ "message": "帳戶已建立!現在可以登入了。" }, "newAccountCreated2": { - "message": "您已成功創建新帳戶!" + "message": "您已成功建立新帳號!" }, "youHaveBeenLoggedIn": { "message": "你已經登入!" @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "如果在您的密碼庫中找不到項目,則詢問是否新增項目。適用於所有已登入的帳戶。" }, - "showCardsInVaultView": { - "message": "在密碼庫介面中顯示支付卡自動填入建議" + "showCardsInVaultViewV2": { + "message": "一律在密碼庫介面中顯示支付卡自動填入建議" }, "showCardsCurrentTab": { "message": "於分頁頁面顯示支付卡" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "於分頁頁面顯示信用卡以便於自動填入。" }, - "showIdentitiesInVaultView": { - "message": "在密碼庫介面中顯示身分自動填入建議" + "showIdentitiesInVaultViewV2": { + "message": "一律在密碼庫介面中顯示身分自動填入建議" }, "showIdentitiesCurrentTab": { "message": "於分頁頁面顯示身分" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "升級為進階會員" }, - "premiumPurchaseAlert": { - "message": "您可以在 bitwarden.com 網頁版密碼庫購買進階會員資格。現在要前往嗎?" - }, "premiumPurchaseAlertV2": { "message": "您可以在 Bitwarden 網頁 App 的帳號設定中購買進階版。" }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "使用者名稱產生器" }, + "useThisEmail": { + "message": "使用此電子郵件" + }, "useThisPassword": { "message": "使用此密碼" }, @@ -2339,11 +2357,11 @@ "blockedDomainsDesc": { "message": "自動填入及其它相關的功能無法在這些網站上使用。您必須重新整理頁面來使變更生效。" }, - "autofillBlockedNotice": { - "message": "自動填入已在此網站被封鎖。請在設定中檢視或更改此限制。" + "autofillBlockedNoticeV2": { + "message": "自動填入已於此網站封鎖" }, - "autofillBlockedTooltip": { - "message": "自動填入已在此網站被封鎖。請在設定中檢視。" + "autofillBlockedNoticeGuidance": { + "message": "您可以於設定中進行更改" }, "websiteItemLabel": { "message": "網站 $number$ (URI)", @@ -2511,7 +2529,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send 創建成功!", + "message": "Send 建立成功!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "已傳送通知至您的裝置。" }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "網頁應用程式" + }, + "notificationSentDevicePart2": { + "message": "在核准前請確保您的指紋短語與下面完全相符。" + }, "aNotificationWasSentToYourDevice": { "message": "已傳送通知至您的裝置" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "請確保您的帳號已解鎖,並且指紋短語與其他裝置一致。" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "一旦您的請求被通過,您會獲得通知。" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "登入已啟動" }, + "logInRequestSent": { + "message": "已傳送請求" + }, "exposedMasterPassword": { "message": "已洩露的主密碼" }, @@ -3425,38 +3452,6 @@ "message": "切換至折疊狀態", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "匯入你的資料至 Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "保護你的 LastPass 資料並匯入至 Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "儲存為未加密的檔案", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "匯入至 Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "匯入中……", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "資料匯入成功!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "匯入時發生錯誤。檢查控制台以了解詳細資訊。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "匯入時遇到網路錯誤", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "別名網域" }, @@ -4010,6 +4005,9 @@ "autofillSuggestions": { "message": "自動填入建議" }, + "itemSuggestions": { + "message": "建議項目" + }, "autofillSuggestionsTip": { "message": "對此網站儲存登入項目為自動填入" }, @@ -4157,15 +4155,6 @@ "itemName": { "message": "項目名稱" }, - "cannotRemoveViewOnlyCollections": { - "message": "若您只有檢視權限,無法移除集合 $COLLECTIONS$。", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "組織已被停用" }, @@ -4586,12 +4575,6 @@ "textSends": { "message": "文字 Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden 有了新外觀!" - }, - "bitwardenNewLookDesc": { - "message": "更容易使用的自動填入及密碼庫搜尋體驗。試試看吧!" - }, "accountActions": { "message": "帳號動作" }, @@ -4903,5 +4886,20 @@ }, "extraWide": { "message": "更寬" + }, + "cannotRemoveViewOnlyCollections": { + "message": "若您只有檢視權限,無法移除集合 $COLLECTIONS$。", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "請更新您的桌面應用程式" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "為了使用生物辨識解鎖,請更新您的桌面應用程式,或在設定中停用指紋解鎖。" } } diff --git a/apps/browser/src/auth/popup/home.component.html b/apps/browser/src/auth/popup/home.component.html index ed3957979612..08043cf88bbf 100644 --- a/apps/browser/src/auth/popup/home.component.html +++ b/apps/browser/src/auth/popup/home.component.html @@ -1,4 +1,4 @@ - +
@@ -30,9 +30,7 @@
diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 6060ac77abec..dbdcf7d5aa84 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -6,7 +6,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; +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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -30,9 +30,6 @@ export class HomeComponent implements OnInit, OnDestroy { rememberEmail: [false], }); - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - constructor( protected platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, @@ -40,7 +37,6 @@ export class HomeComponent implements OnInit, OnDestroy { private i18nService: I18nService, private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, - private registerRouteService: RegisterRouteService, private toastService: ToastService, private configService: ConfigService, private route: ActivatedRoute, @@ -118,13 +114,15 @@ export class HomeComponent implements OnInit, OnDestroy { } await this.setLoginEmailValues(); - await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); + await this.router.navigate(["login"], { + queryParams: { email: this.formGroup.controls.email.value }, + }); } 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); + this.loginEmailService.setRememberEmail(this.formGroup.controls.rememberEmail.value); + await this.loginEmailService.setLoginEmail(this.formGroup.controls.email.value); await this.loginEmailService.saveEmailSettings(); } } diff --git a/apps/browser/src/auth/popup/login-v1.component.ts b/apps/browser/src/auth/popup/login-v1.component.ts index cb6380d62ede..b2c52f248c6b 100644 --- a/apps/browser/src/auth/popup/login-v1.component.ts +++ b/apps/browser/src/auth/popup/login-v1.component.ts @@ -10,11 +10,9 @@ 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 { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -51,8 +49,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit { route: ActivatedRoute, loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - webAuthnLoginService: WebAuthnLoginServiceAbstraction, - registerRouteService: RegisterRouteService, toastService: ToastService, ) { super( @@ -73,8 +69,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit { route, loginEmailService, ssoLoginService, - webAuthnLoginService, - registerRouteService, toastService, ); this.onSuccessfulLogin = async () => { diff --git a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html index e7fafbb252c7..34c0cbe9614f 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html +++ b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html @@ -7,13 +7,20 @@

-

{{ "loginInitiated" | i18n }}

+

{{ "logInRequestSent" | i18n }}

-

{{ "notificationSentDevice" | i18n }}

-

- {{ "fingerprintMatchInfo" | i18n }} + {{ "notificationSentDevicePart1" | i18n }} + {{ "notificationSentDeviceAnchor" | i18n }}. {{ "notificationSentDevicePart2" | i18n }}

diff --git a/apps/browser/src/auth/popup/register.component.ts b/apps/browser/src/auth/popup/register.component.ts index f8a332f0fd1f..50475b2204dc 100644 --- a/apps/browser/src/auth/popup/register.component.ts +++ b/apps/browser/src/auth/popup/register.component.ts @@ -13,9 +13,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi 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, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KeyService } from "@bitwarden/key-management"; @Component({ @@ -34,9 +32,7 @@ export class RegisterComponent extends BaseRegisterComponent { i18nService: I18nService, keyService: KeyService, apiService: ApiService, - stateService: StateService, platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, environmentService: EnvironmentService, logService: LogService, auditService: AuditService, @@ -51,9 +47,7 @@ export class RegisterComponent extends BaseRegisterComponent { i18nService, keyService, apiService, - stateService, platformUtilsService, - passwordGenerationService, environmentService, logService, auditService, 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 index 53aedc7a5f37..687dc6839298 100644 --- a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts @@ -27,9 +27,6 @@ import { FormFieldModule } from "../../../../../libs/components/src/form-field"; import { LinkModule } from "../../../../../libs/components/src/link"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports -import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; @@ -50,7 +47,6 @@ import { ZonedMessageListenerService } from "../../platform/browser/zoned-messag AsyncActionsModule, FormsModule, ], - providers: [I18nPipe], }) export class TwoFactorAuthDuoComponent extends TwoFactorAuthDuoBaseComponent 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 index 723152adfaba..7afe1eb889ec 100644 --- a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts @@ -23,9 +23,6 @@ import { FormFieldModule } from "../../../../../libs/components/src/form-field"; import { LinkModule } from "../../../../../libs/components/src/link"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports -import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; @@ -46,7 +43,6 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; AsyncActionsModule, FormsModule, ], - providers: [I18nPipe], }) export class TwoFactorAuthEmailComponent extends TwoFactorAuthEmailBaseComponent implements OnInit { private dialogService = inject(DialogService); diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index a2f9cd9d0fc9..43230bd23f43 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -33,7 +33,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; -import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window"; +import { closeTwoFactorAuthWebAuthnPopout } from "./utils/auth-popout-window"; @Component({ selector: "app-two-factor", @@ -171,7 +171,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit // We don't need this window anymore because the intent is for the user to be left // on the web vault screen which tells them to continue in the browser extension (sidebar or popup) - await closeTwoFactorAuthPopout(); + await closeTwoFactorAuthWebAuthnPopout(); }; } }); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts index 9e7d69fad974..deb71f73cd6a 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts @@ -7,8 +7,9 @@ import { openUnlockPopout, closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, - closeTwoFactorAuthPopout, + openTwoFactorAuthWebAuthnPopout, + closeTwoFactorAuthWebAuthnPopout, + closeSsoAuthResultPopout, } from "./auth-popout-window"; describe("AuthPopoutWindow", () => { @@ -97,22 +98,30 @@ describe("AuthPopoutWindow", () => { }); }); - describe("openTwoFactorAuthPopout", () => { - it("opens a window that facilitates two factor authentication", async () => { - await openTwoFactorAuthPopout({ data: "data", remember: "remember" }); + describe("closeSsoAuthResultPopout", () => { + it("closes the SSO authentication result popout window", async () => { + await closeSsoAuthResultPopout(); + + expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.ssoAuthResult); + }); + }); + + describe("openTwoFactorAuthWebAuthnPopout", () => { + it("opens a window that facilitates two factor authentication via WebAuthn", async () => { + await openTwoFactorAuthWebAuthnPopout({ data: "data", remember: "remember" }); expect(openPopoutSpy).toHaveBeenCalledWith( "popup/index.html#/2fa;webAuthnResponse=data;remember=remember", - { singleActionKey: AuthPopoutType.twoFactorAuth }, + { singleActionKey: AuthPopoutType.twoFactorAuthWebAuthn }, ); }); }); - describe("closeTwoFactorAuthPopout", () => { - it("closes the two-factor authentication window", async () => { - await closeTwoFactorAuthPopout(); + describe("closeTwoFactorAuthWebAuthnPopout", () => { + it("closes the two-factor authentication WebAuthn window", async () => { + await closeTwoFactorAuthWebAuthnPopout(); - expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.twoFactorAuth); + expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.twoFactorAuthWebAuthn); }); }); }); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.ts index 5a0e577807f2..8d6e7fa92cd7 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.ts @@ -6,7 +6,7 @@ import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; const AuthPopoutType = { unlockExtension: "auth_unlockExtension", ssoAuthResult: "auth_ssoAuthResult", - twoFactorAuth: "auth_twoFactorAuth", + twoFactorAuthWebAuthn: "auth_twoFactorAuthWebAuthn", } as const; const extensionUnlockUrls = new Set([ chrome.runtime.getURL("popup/index.html#/lock"), @@ -60,26 +60,37 @@ async function openSsoAuthResultPopout(resultData: { code: string; state: string } /** - * Opens a window that facilitates two-factor authentication. + * Closes the SSO authentication result popout window. + */ +async function closeSsoAuthResultPopout() { + await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.ssoAuthResult); +} + +/** + * Opens a popout that facilitates two-factor authentication via WebAuthn. * - * @param twoFactorAuthData - The data from the two-factor authentication. + * @param twoFactorAuthWebAuthnData - The data to send ot the popout via query param. + * It includes the WebAuthn response and whether to save the 2FA remember me token or not. */ -async function openTwoFactorAuthPopout(twoFactorAuthData: { data: string; remember: string }) { - const { data, remember } = twoFactorAuthData; +async function openTwoFactorAuthWebAuthnPopout(twoFactorAuthWebAuthnData: { + data: string; + remember: string; +}) { + const { data, remember } = twoFactorAuthWebAuthnData; const params = `webAuthnResponse=${encodeURIComponent(data)};` + `remember=${encodeURIComponent(remember)}`; const twoFactorUrl = `popup/index.html#/2fa;${params}`; await BrowserPopupUtils.openPopout(twoFactorUrl, { - singleActionKey: AuthPopoutType.twoFactorAuth, + singleActionKey: AuthPopoutType.twoFactorAuthWebAuthn, }); } /** * Closes the two-factor authentication popout window. */ -async function closeTwoFactorAuthPopout() { - await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuth); +async function closeTwoFactorAuthWebAuthnPopout() { + await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuthWebAuthn); } export { @@ -87,6 +98,7 @@ export { openUnlockPopout, closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, - closeTwoFactorAuthPopout, + closeSsoAuthResultPopout, + openTwoFactorAuthWebAuthnPopout, + closeTwoFactorAuthWebAuthnPopout, }; diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index ed9d8e6d84b5..1b989283112f 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -31,16 +31,10 @@ interface AddUnlockVaultQueueMessage extends NotificationQueueMessage { type: "unlock"; } -interface AddRequestFilelessImportQueueMessage extends NotificationQueueMessage { - type: "fileless-import"; - importType?: string; -} - type NotificationQueueMessageItem = | AddLoginQueueMessage | AddChangePasswordQueueMessage - | AddUnlockVaultQueueMessage - | AddRequestFilelessImportQueueMessage; + | AddUnlockVaultQueueMessage; type LockedVaultPendingNotificationsData = { commandToRetry: { @@ -122,7 +116,6 @@ export { AddChangePasswordQueueMessage, AddLoginQueueMessage, AddUnlockVaultQueueMessage, - AddRequestFilelessImportQueueMessage, NotificationQueueMessageItem, LockedVaultPendingNotificationsData, AdjustNotificationBarMessageData, diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 5c6ff3c2c8c5..a091256b28d7 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -14,6 +14,7 @@ import { } 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; @@ -24,6 +25,7 @@ import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-stat import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; 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"; @@ -31,13 +33,13 @@ 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 { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; +import { NotificationCipherData } from "../content/components/cipher/types"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; import { AddChangePasswordQueueMessage, AddLoginQueueMessage, - AddRequestFilelessImportQueueMessage, AddUnlockVaultQueueMessage, ChangePasswordMessageData, AddLoginMessageData, @@ -81,6 +83,8 @@ export default class NotificationBackground { bgGetExcludedDomains: () => this.getExcludedDomains(), bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(), + notificationRefreshFlagValue: () => this.getNotificationFlag(), + bgGetDecryptedCiphers: () => this.getNotificationCipherData(), }; private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); @@ -131,6 +135,40 @@ export default class NotificationBackground { return await firstValueFrom(this.domainSettingsService.neverDomains$); } + /** + * + * Gets the current active tab and retrieves all decrypted ciphers + * for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects. + * If no active tab or URL is found, it returns an empty array. + * + * @returns {Promise} + */ + + async getNotificationCipherData(): Promise { + const [currentTab, showFavicons, env] = await Promise.all([ + BrowserApi.getTabFromCurrentWindow(), + firstValueFrom(this.domainSettingsService.showFavicons$), + firstValueFrom(this.environmentService.environment$), + ]); + const iconsServerUrl = env.getIconsUrl(); + const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl(currentTab.url); + + return decryptedCiphers.map((view) => { + const { id, name, reprompt, favorite, login } = view; + return { + id, + name, + type: CipherType.Login, + reprompt, + favorite, + icon: buildCipherIcon(iconsServerUrl, view, showFavicons), + login: login && { + username: login.username, + }, + }; + }); + } + /** * Gets the active user server config from the config service. */ @@ -138,6 +176,15 @@ export default class NotificationBackground { return await firstValueFrom(this.configService.serverConfig$); } + /** + * Gets the current value of the notification refresh feature flag + * @returns Promise indicating if the feature is enabled + */ + async getNotificationFlag(): Promise { + const flagValue = await this.configService.getFeatureFlag(FeatureFlag.NotificationRefresh); + return flagValue; + } + private async getAuthStatus() { return await firstValueFrom(this.authService.activeAccountStatus$); } @@ -201,11 +248,6 @@ export default class NotificationBackground { case NotificationQueueMessageType.AddLogin: typeData.removeIndividualVault = await this.removeIndividualVault(); break; - case NotificationQueueMessageType.RequestFilelessImport: - typeData.importType = ( - notificationQueueMessage as AddRequestFilelessImportQueueMessage - ).importType; - break; } await BrowserApi.tabSendMessageData(tab, "openNotificationBar", { @@ -399,25 +441,6 @@ export default class NotificationBackground { } } - /** - * Sets up a notification to request a fileless import when the user - * attempts to trigger an import from a third party website. - * - * @param tab - The tab that we are sending the notification to - * @param importType - The type of import that is being requested - */ - async requestFilelessImport(tab: chrome.tabs.Tab, importType: string) { - const currentAuthStatus = await this.getAuthStatus(); - if (currentAuthStatus !== AuthenticationStatus.Unlocked || this.notificationQueue.length) { - return; - } - - const loginDomain = Utils.getDomain(tab.url); - if (loginDomain) { - await this.pushRequestFilelessImportToQueue(loginDomain, tab, importType); - } - } - private async pushChangePasswordToQueue( cipherId: string, loginDomain: string, @@ -456,36 +479,6 @@ export default class NotificationBackground { await this.sendNotificationQueueMessage(tab, message); } - /** - * Pushes a request to start a fileless import to the notification queue. - * This will display a notification bar to the user, prompting them to - * start the import. - * - * @param loginDomain - The domain of the tab that we are sending the notification to - * @param tab - The tab that we are sending the notification to - * @param importType - The type of import that is being requested - */ - private async pushRequestFilelessImportToQueue( - loginDomain: string, - tab: chrome.tabs.Tab, - importType?: string, - ) { - this.removeTabFromNotificationQueue(tab); - const launchTimestamp = new Date().getTime(); - const message: AddRequestFilelessImportQueueMessage = { - type: NotificationQueueMessageType.RequestFilelessImport, - domain: loginDomain, - tab, - launchTimestamp, - expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds - wasVaultLocked: false, - importType, - }; - this.notificationQueue.push(message); - await this.checkNotificationQueue(tab); - this.removeTabFromNotificationQueue(tab); - } - /** * Saves a cipher based on the message sent from the notification bar. If the vault * is locked, the message will be added to the notification queue and the unlock diff --git a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts index 9e2da59d992c..9068bbfc27d6 100644 --- a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts +++ b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts @@ -1,14 +1,14 @@ -import { dirname, join } from "path"; -import path from "path"; +import path, { dirname, join } from "path"; + import type { StorybookConfig } from "@storybook/web-components-webpack5"; -import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; import remarkGfm from "remark-gfm"; +import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; const getAbsolutePath = (value: string): string => dirname(require.resolve(join(value, "package.json"))); const config: StorybookConfig = { - stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)"], + stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)", "../lit-stories/**/*.mdx"], addons: [ getAbsolutePath("@storybook/addon-links"), getAbsolutePath("@storybook/addon-essentials"), diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts index de374b44a97a..6ff32353938c 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts @@ -6,10 +6,10 @@ import { Theme } from "@bitwarden/common/platform/enums"; import { themes, typography } from "../../../content/components/constants/styles"; import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons"; -import { CipherData } from "./types"; +import { NotificationCipherData } from "./types"; // @TODO support other cipher types (card, identity, notes, etc) -export function CipherInfo({ cipher, theme }: { cipher: CipherData; theme: Theme }) { +export function CipherInfo({ cipher, theme }: { cipher: NotificationCipherData; theme: Theme }) { const { name, login } = cipher; return html` diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts index 651c20cac3a4..96b44d2c0cc3 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts @@ -12,7 +12,7 @@ import { import { CipherAction } from "./cipher-action"; import { CipherIcon } from "./cipher-icon"; import { CipherInfo } from "./cipher-info"; -import { CipherData } from "./types"; +import { NotificationCipherData } from "./types"; const cipherIconWidth = "24px"; @@ -22,7 +22,7 @@ export function CipherItem({ notificationType, theme = ThemeTypes.Light, }: { - cipher: CipherData; + cipher: NotificationCipherData; handleAction?: (e: Event) => void; notificationType?: NotificationType; theme: Theme; diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts index acdee756570a..ff29f9b559f1 100644 --- a/apps/browser/src/autofill/content/components/cipher/types.ts +++ b/apps/browser/src/autofill/content/components/cipher/types.ts @@ -1,6 +1,4 @@ -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const CipherTypes = { +export const CipherTypes = { Login: 1, SecureNote: 2, Card: 3, @@ -9,9 +7,7 @@ const CipherTypes = { type CipherType = (typeof CipherTypes)[keyof typeof CipherTypes]; -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const CipherRepromptTypes = { +export const CipherRepromptTypes = { None: 0, Password: 1, } as const; @@ -25,13 +21,16 @@ export type WebsiteIconData = { icon: string; }; -export type CipherData = { +type BaseCipherData = { id: string; name: string; - type: CipherType; + type: CipherTypeValue; reprompt: CipherRepromptType; favorite: boolean; icon: WebsiteIconData; +}; + +export type CipherData = BaseCipherData & { accountCreationFieldType?: string; login?: { username: string; @@ -46,3 +45,9 @@ export type CipherData = { username?: string; }; }; + +export type NotificationCipherData = BaseCipherData & { + login?: { + username: string; + }; +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx new file mode 100644 index 000000000000..d3c1968b32f9 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx @@ -0,0 +1,64 @@ +import { Meta, Controls, Primary } from "@storybook/addon-docs"; + +import * as stories from "./action-button.lit-stories"; + + + +## Action Button + +The `ActionButton` component is a customizable button built using the `lit` library and styled with +`@emotion/css`. This component supports themes, handles click events, and includes a disabled state. +It is designed with accessibility and responsive design in mind. + + + + +## Props + +| **Prop** | **Type** | **Required** | **Description** | +| -------------- | -------------------------- | ------------ | ----------------------------------------------------------- | +| `buttonAction` | `(e: Event) => void` | Yes | The function to execute when the button is clicked. | +| `buttonText` | `string` | Yes | The text to display on the button. | +| `disabled` | `boolean` (default: false) | No | Disables the button when set to `true`. | +| `theme` | `Theme` | Yes | The theme to style the button. Must match the `Theme` enum. | + +## Installation and Setup + +1. Ensure you have the necessary dependencies installed: + + - `lit`: Used to render the component. + - `@emotion/css`: Used for styling the component. + +2. Pass the required props to the component when rendering: + - `buttonAction`: A function that handles the click event. + - `buttonText`: The text displayed on the button. + - `disabled` (optional): A boolean indicating whether the button is disabled. + - `theme`: The theme to style the button (must be a valid `Theme`). + +## Accessibility (WCAG) Compliance + +The `ActionButton` component follows the +[W3C ARIA button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/). Below is a breakdown of +key accessibility considerations: + +### Keyboard Accessibility + +- The button supports keyboard interaction through the `@click` event. +- Users can activate the button using the `Enter` or `Space` key. + +### Screen Reader Compatibility + +- The `title` attribute is dynamically set to the button's text (`buttonText`), ensuring it is read + by screen readers. +- The semantic `
- - diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 2c0ebe8e8e7b..2316df198573 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { render } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { FilelessImportPort, FilelessImportType } from "../../tools/enums/fileless-import.enums"; import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; +import { NotificationContainer } from "../content/components/notification/container"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -13,15 +15,13 @@ import { NotificationBarWindowMessageHandlers, NotificationBarWindowMessage, NotificationBarIframeInitData, + NotificationType, } from "./abstractions/notification-bar"; -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./bar.scss"); - const logService = new ConsoleLogService(false); let notificationBarIframeInitData: NotificationBarIframeInitData = {}; let windowMessageOrigin: string; +let useComponentBar = false; const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = { initNotificationBar: ({ message }) => initNotificationBar(message), saveCipherAttemptCompleted: ({ message }) => handleSaveCipherAttemptCompletedMessage(message), @@ -30,6 +30,17 @@ const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers globalThis.addEventListener("load", load); function load() { setupWindowMessageListener(); + sendPlatformMessage({ command: "notificationRefreshFlagValue" }, (flagValue) => { + useComponentBar = flagValue; + applyNotificationBarStyle(); + }); +} + +function applyNotificationBarStyle() { + if (!useComponentBar) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + require("./bar.scss"); + } postMessageToParent({ command: "initNotificationBar" }); } @@ -40,12 +51,7 @@ function initNotificationBar(message: NotificationBarWindowMessage) { } notificationBarIframeInitData = initData; - const { isVaultLocked } = notificationBarIframeInitData; - setNotificationBarTheme(); - - (document.getElementById("logo") as HTMLImageElement).src = isVaultLocked - ? chrome.runtime.getURL("images/icon38_locked.png") - : chrome.runtime.getURL("images/icon38.png"); + const { isVaultLocked, theme } = notificationBarIframeInitData; const i18n = { appName: chrome.i18n.getMessage("appName"), @@ -59,13 +65,52 @@ function initNotificationBar(message: NotificationBarWindowMessage) { notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), - filelessImport: chrome.i18n.getMessage("filelessImport"), - lpFilelessImport: chrome.i18n.getMessage("lpFilelessImport"), - cancelFilelessImport: chrome.i18n.getMessage("no"), - lpCancelFilelessImport: chrome.i18n.getMessage("lpCancelFilelessImport"), - startFilelessImport: chrome.i18n.getMessage("startFilelessImport"), + + // @TODO move values to message catalog + saveAction: "Save", + saveAsNewLoginAction: "Save as new login", + updateLoginAction: "Update login", + saveLoginPrompt: "Save login?", + updateLoginPrompt: "Update existing login?", + loginSaveSuccess: "Login saved", + loginSaveSuccessDetails: "Login saved to Bitwarden.", + loginUpdateSuccess: "Login saved", + loginUpdateSuccessDetails: "Login updated in Bitwarden.", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details as a New item", }; + if (useComponentBar) { + document.body.innerHTML = ""; + // Current implementations utilize a require for scss files which creates the need to remove the node. + document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove()); + const themeType = getTheme(globalThis, theme); + + // There are other possible passed theme values, but for now, resolve to dark or light + const resolvedTheme: Theme = themeType === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light; + + sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => { + // @TODO use context to avoid prop drilling + return render( + NotificationContainer({ + ...notificationBarIframeInitData, + type: notificationBarIframeInitData.type as NotificationType, + theme: resolvedTheme, + handleCloseNotification, + i18n, + ciphers: cipherData, + }), + document.body, + ); + }); + } + + setNotificationBarTheme(); + + (document.getElementById("logo") as HTMLImageElement).src = isVaultLocked + ? chrome.runtime.getURL("images/icon38_locked.png") + : chrome.runtime.getURL("images/icon38.png"); + setupLogoLink(i18n); // i18n for "Add" template @@ -107,48 +152,32 @@ function initNotificationBar(message: NotificationBarWindowMessage) { unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc; - // i18n for "Fileless Import" (fileless-import) template - const isLpImport = initData.importType === FilelessImportType.LP; - const importTemplate = document.getElementById("template-fileless-import") as HTMLTemplateElement; - - const startImportButton = importTemplate.content.getElementById("start-fileless-import"); - startImportButton.textContent = i18n.startFilelessImport; - - const cancelImportButton = importTemplate.content.getElementById("cancel-fileless-import"); - cancelImportButton.textContent = isLpImport - ? i18n.lpCancelFilelessImport - : i18n.cancelFilelessImport; - - importTemplate.content.getElementById("fileless-import-text").textContent = isLpImport - ? i18n.lpFilelessImport - : i18n.filelessImport; - // i18n for body content const closeButton = document.getElementById("close-button"); closeButton.title = i18n.close; const notificationType = initData.type; - if (initData.type === "add") { + if (notificationType === "add") { handleTypeAdd(); } else if (notificationType === "change") { handleTypeChange(); } else if (notificationType === "unlock") { handleTypeUnlock(); - } else if (notificationType === "fileless-import") { - handleTypeFilelessImport(); } - closeButton.addEventListener("click", (e) => { - e.preventDefault(); - sendPlatformMessage({ - command: "bgCloseNotificationBar", - }); - }); + closeButton.addEventListener("click", handleCloseNotification); globalThis.addEventListener("resize", adjustHeight); adjustHeight(); } +function handleCloseNotification(e: Event) { + e.preventDefault(); + sendPlatformMessage({ + command: "bgCloseNotificationBar", + }); +} + function handleTypeAdd() { setContent(document.getElementById("template-add") as HTMLTemplateElement); @@ -249,59 +278,6 @@ function handleTypeUnlock() { }); } -/** - * Sets up a port to communicate with the fileless importer content script. - * This connection to the background script is used to trigger the action of - * downloading the CSV file from the LP importer or importing the data into - * the Bitwarden vault. - */ -function handleTypeFilelessImport() { - const importType = notificationBarIframeInitData.importType; - const port = chrome.runtime.connect({ name: FilelessImportPort.NotificationBar }); - setContent(document.getElementById("template-fileless-import") as HTMLTemplateElement); - - const startFilelessImportButton = document.getElementById("start-fileless-import"); - const startFilelessImport = () => { - port.postMessage({ command: "startFilelessImport", importType }); - document.getElementById("fileless-import-buttons").textContent = - chrome.i18n.getMessage("importing"); - startFilelessImportButton.removeEventListener("click", startFilelessImport); - }; - startFilelessImportButton.addEventListener("click", startFilelessImport); - - const cancelFilelessImportButton = document.getElementById("cancel-fileless-import"); - cancelFilelessImportButton.addEventListener("click", () => { - port.postMessage({ command: "cancelFilelessImport", importType }); - }); - - const handlePortMessage = (msg: any) => { - if (msg.command !== "filelessImportCompleted" && msg.command !== "filelessImportFailed") { - return; - } - - port.disconnect(); - - const filelessImportButtons = document.getElementById("fileless-import-buttons"); - const notificationBarOuterWrapper = document.getElementById("notification-bar-outer-wrapper"); - - if (msg.command === "filelessImportCompleted") { - filelessImportButtons.textContent = chrome.i18n.getMessage("dataSuccessfullyImported"); - filelessImportButtons.prepend(buildSvgDomElement(circleCheckIcon)); - filelessImportButtons.classList.add("success-message"); - notificationBarOuterWrapper.classList.add("success-event"); - adjustHeight(); - return; - } - - filelessImportButtons.textContent = chrome.i18n.getMessage("dataImportFailed"); - filelessImportButtons.classList.add("error-message"); - notificationBarOuterWrapper.classList.add("error-event"); - adjustHeight(); - logService.error(`Error Encountered During Import: ${msg.importErrorMessage}`); - }; - port.onMessage.addListener(handlePortMessage); -} - function setContent(template: HTMLTemplateElement) { const content = document.getElementById("content"); while (content.firstChild) { @@ -394,14 +370,19 @@ function setupLogoLink(i18n: Record) { sendPlatformMessage({ command: "getWebVaultUrlForNotification" }, setWebVaultUrlLink); } -function setNotificationBarTheme() { - let theme = notificationBarIframeInitData.theme; +function getTheme(globalThis: any, theme: NotificationBarIframeInitData["theme"]) { if (theme === ThemeTypes.System) { - theme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches + return globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? ThemeTypes.Dark : ThemeTypes.Light; } + return theme; +} + +function setNotificationBarTheme() { + const theme = getTheme(globalThis, notificationBarIframeInitData.theme); + document.documentElement.classList.add(`theme_${theme}`); if (notificationBarIframeInitData.applyRedesign) { diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.html index 9f6c0aca50de..45c0612af446 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.html @@ -47,6 +47,6 @@
diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index eeae0a85e3f2..e8299f011664 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -118,7 +118,7 @@

{{ "autofillSuggestionsSectionTitle" | i18n }}

(change)="updateShowCardsCurrentTab()" [(ngModel)]="showCardsCurrentTab" /> - {{ "showCardsInVaultView" | i18n }} + {{ "showCardsInVaultViewV2" | i18n }} {{ "autofillSuggestionsSectionTitle" | i18n }}

[(ngModel)]="showIdentitiesCurrentTab" /> - {{ "showIdentitiesInVaultView" | i18n }} + {{ "showIdentitiesInVaultViewV2" | i18n }} 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 acf0dedde273..6757972e839c 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -424,6 +424,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } await this.setupSubmitListenerOnFormlessField(formFieldElement); + return; } /** @@ -439,15 +440,16 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.formElements.add(formElement); formElement.addEventListener(EVENTS.SUBMIT, this.handleFormFieldSubmitEvent); - const closesSubmitButton = await this.findSubmitButton(formElement); + const closestSubmitButton = await this.findSubmitButton(formElement); // If we cannot find a submit button within the form, check for a submit button outside the form. - if (!closesSubmitButton) { + if (!closestSubmitButton) { await this.setupSubmitListenerOnFormlessField(formFieldElement); return; } - this.setupSubmitButtonEventListeners(closesSubmitButton); + this.setupSubmitButtonEventListeners(closestSubmitButton); + return; } } @@ -459,9 +461,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ */ private async setupSubmitListenerOnFormlessField(formFieldElement: FillableFormFieldElement) { if (formFieldElement && !this.fieldsWithSubmitElements.has(formFieldElement)) { - const closesSubmitButton = await this.findClosestFormlessSubmitButton(formFieldElement); - this.setupSubmitButtonEventListeners(closesSubmitButton); + const closestSubmitButton = await this.findClosestFormlessSubmitButton(formFieldElement); + + this.setupSubmitButtonEventListeners(closestSubmitButton); } + 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 7471c2989179..5f3bc4dfff94 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 @@ -317,6 +317,7 @@ describe("CollectAutofillContentService", () => { __form__0: { opid: "__form__0", htmlAction: formAction, + htmlClass: null, htmlName: formName, htmlID: formId, htmlMethod: formMethod, @@ -544,6 +545,7 @@ describe("CollectAutofillContentService", () => { __form__0: { opid: "__form__0", htmlAction: formAction1, + htmlClass: null, htmlName: formName1, htmlID: formId1, htmlMethod: formMethod1, @@ -551,6 +553,7 @@ describe("CollectAutofillContentService", () => { __form__1: { opid: "__form__1", htmlAction: formAction2, + htmlClass: null, htmlName: formName2, htmlID: formId2, htmlMethod: formMethod2, 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 13c546bccdbf..0f9c8993014f 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -228,6 +228,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ opid: formElement.opid, htmlAction: this.getFormActionAttribute(formElement), htmlName: this.getPropertyOrAttribute(formElement, "name"), + htmlClass: this.getPropertyOrAttribute(formElement, "class"), htmlID: this.getPropertyOrAttribute(formElement, "id"), htmlMethod: this.getPropertyOrAttribute(formElement, "method"), }); 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 43efd338c6e2..dff40a3ab93c 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 @@ -56,6 +56,7 @@ export class InlineMenuFieldQualificationService "neuer benutzer", "neues passwort", "neue e-mail", + "pwdcheck", ]; private updatePasswordFieldKeywords = [ "update password", diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 0e102dcfd996..614a5b014f2e 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -37,7 +37,9 @@ export function requestIdleCallbackPolyfill( return globalThis.requestIdleCallback(() => callback(), options); } - return globalThis.setTimeout(() => callback(), 1); + const timeoutDelay = options?.timeout || 1; + + return globalThis.setTimeout(() => callback(), timeoutDelay); } /** diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 9e10ed974ba5..08d4b9fc00cd 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -2,11 +2,11 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; -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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; const IdleInterval = 60 * 5; // 5 minutes diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5d09122bbd64..68fa65f85284 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs"; +import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs"; import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; import { @@ -18,14 +18,13 @@ import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstracti import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; @@ -76,12 +75,16 @@ import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/bill import { ClientType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation"; +import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; +import { FallbackBulkEncryptService } from "@bitwarden/common/key-management/crypto/services/fallback-bulk-encrypt.service"; +import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation"; import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.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"; @@ -92,6 +95,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 { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { @@ -108,16 +112,21 @@ 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 { NotificationsService } from "@bitwarden/common/platform/notifications"; +// eslint-disable-next-line no-restricted-imports -- Needed for service creation +import { + DefaultNotificationsService, + SignalRConnectionService, + UnsupportedWebPushConnectionService, + WebPushNotificationsApiService, + WorkerWebPushConnectionService, +} from "@bitwarden/common/platform/notifications/internal"; 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"; @@ -125,6 +134,7 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo 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 { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StateService } from "@bitwarden/common/platform/services/state.service"; @@ -157,7 +167,6 @@ import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { @@ -189,23 +198,23 @@ import { FolderService } from "@bitwarden/common/vault/services/folder/folder.se import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { - PasswordGenerationServiceAbstraction, - UsernameGenerationServiceAbstraction, legacyPasswordGenerationServiceFactory, legacyUsernameGenerationServiceFactory, + PasswordGenerationServiceAbstraction, + UsernameGenerationServiceAbstraction, } from "@bitwarden/generator-legacy"; import { ImportApiService, ImportApiServiceAbstraction, ImportService, ImportServiceAbstraction, -} from "@bitwarden/importer/core"; +} from "@bitwarden/importer-core"; import { - BiometricStateService, BiometricsService, + BiometricStateService, DefaultBiometricStateService, - DefaultKeyService, DefaultKdfConfigService, + DefaultKeyService, KdfConfigService, KeyService as KeyServiceAbstraction, } from "@bitwarden/key-management"; @@ -260,7 +269,7 @@ import { LocalBackedSessionStorageService } from "../platform/services/local-bac 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 { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory"; +import { BrowserSdkLoadService } from "../platform/services/sdk/browser-sdk-load.service"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; @@ -268,7 +277,6 @@ import { OffscreenStorageService } from "../platform/storage/offscreen-storage.s 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 { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -313,7 +321,7 @@ export default class MainBackground { importService: ImportServiceAbstraction; exportService: VaultExportServiceAbstraction; searchService: SearchServiceAbstraction; - notificationsService: NotificationsServiceAbstraction; + notificationsService: NotificationsService; stateService: StateServiceAbstraction; userNotificationSettingsService: UserNotificationSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction; @@ -377,9 +385,12 @@ export default class MainBackground { kdfConfigService: KdfConfigService; offscreenDocumentService: OffscreenDocumentService; syncServiceListener: SyncServiceListener; + + webPushConnectionService: WorkerWebPushConnectionService | UnsupportedWebPushConnectionService; themeStateService: DefaultThemeStateService; autoSubmitLoginBackground: AutoSubmitLoginBackground; sdkService: SdkService; + sdkLoadService: SdkLoadService; cipherAuthorizationService: CipherAuthorizationService; inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; @@ -393,7 +404,6 @@ export default class MainBackground { private notificationBackground: NotificationBackground; private overlayBackground: OverlayBackgroundInterface; private overlayNotificationsBackground: OverlayNotificationsBackgroundInterface; - private filelessImporterBackground: FilelessImporterBackground; private runtimeBackground: RuntimeBackground; private tabsBackground: TabsBackground; private webRequestBackground: WebRequestBackground; @@ -407,11 +417,6 @@ export default class MainBackground { constructor() { // Services const lockedCallback = async (userId?: string) => { - if (this.notificationsService != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.notificationsService.updateConnection(false); - } await this.refreshBadge(); await this.refreshMenu(true); if (this.systemService != null) { @@ -556,6 +561,7 @@ export default class MainBackground { this.messagingService, this.logService, this.globalStateProvider, + this.singleUserStateProvider, ); this.activeUserStateProvider = new DefaultActiveUserStateProvider( this.accountService, @@ -671,7 +677,7 @@ export default class MainBackground { this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - this.organizationService = new OrganizationService(this.stateProvider); + this.organizationService = new DefaultOrganizationService(this.stateProvider); this.policyService = new PolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( @@ -732,8 +738,9 @@ export default class MainBackground { ); const sdkClientFactory = flagEnabled("sdk") - ? new BrowserSdkClientFactory(this.logService) + ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); + this.sdkLoadService = new BrowserSdkLoadService(this.logService); this.sdkService = new DefaultSdkService( sdkClientFactory, this.environmentService, @@ -802,7 +809,7 @@ export default class MainBackground { this.apiService, ); - this.ssoLoginService = new SsoLoginService(this.stateProvider); + this.ssoLoginService = new SsoLoginService(this.stateProvider, this.logService); this.userVerificationApiService = new UserVerificationApiService(this.apiService); @@ -1032,17 +1039,34 @@ export default class MainBackground { this.organizationVaultExportService, ); - this.notificationsService = new NotificationsService( + if (BrowserApi.isManifestVersion(3)) { + const registration = (self as unknown as { registration: ServiceWorkerRegistration }) + ?.registration; + + if (registration != null) { + this.webPushConnectionService = new WorkerWebPushConnectionService( + this.configService, + new WebPushNotificationsApiService(this.apiService, this.appIdService), + registration, + ); + } else { + this.webPushConnectionService = new UnsupportedWebPushConnectionService(); + } + } else { + this.webPushConnectionService = new UnsupportedWebPushConnectionService(); + } + + this.notificationsService = new DefaultNotificationsService( this.logService, this.syncService, this.appIdService, - this.apiService, this.environmentService, logoutCallback, - this.stateService, - this.authService, this.messagingService, - this.taskSchedulerService, + this.accountService, + new SignalRConnectionService(this.apiService, this.logService), + this.authService, + this.webPushConnectionService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); @@ -1160,16 +1184,6 @@ export default class MainBackground { this.notificationBackground, ); - this.filelessImporterBackground = new FilelessImporterBackground( - this.configService, - this.authService, - this.policyService, - this.notificationBackground, - this.importService, - this.syncService, - this.scriptInjectorService, - ); - this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( this.logService, this.autofillService, @@ -1260,14 +1274,19 @@ export default class MainBackground { this.cipherAuthorizationService = new DefaultCipherAuthorizationService( this.collectionService, this.organizationService, + this.accountService, ); this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); } async bootstrap() { + if (this.webPushConnectionService instanceof WorkerWebPushConnectionService) { + this.webPushConnectionService.start(); + } this.containerService.attachToGlobal(self); + await this.sdkLoadService.load(); // Only the "true" background should run migrations await this.stateService.init({ runMigrations: true }); @@ -1296,7 +1315,6 @@ export default class MainBackground { await this.runtimeBackground.init(); await this.notificationBackground.init(); this.overlayNotificationsBackground.init(); - this.filelessImporterBackground.init(); this.commandsBackground.init(); this.contextMenusBackground?.init(); this.idleBackground.init(); @@ -1331,12 +1349,7 @@ export default class MainBackground { setTimeout(async () => { await this.refreshBadge(); await this.fullSync(false); - this.taskSchedulerService.setInterval( - ScheduledTaskNames.scheduleNextSyncInterval, - 5 * 60 * 1000, // check every 5 minutes - ); - setTimeout(() => this.notificationsService.init(), 2500); - await this.taskSchedulerService.verifyAlarmsState(); + this.notificationsService.startListening(); resolve(); }, 500); }); @@ -1415,7 +1428,6 @@ export default class MainBackground { ForceSetPasswordReason.None; await this.systemService.clearPendingClipboard(); - await this.notificationsService.updateConnection(false); if (nextAccountStatus === AuthenticationStatus.LoggedOut) { this.messagingService.send("goHome"); @@ -1519,7 +1531,6 @@ export default class MainBackground { } await this.refreshBadge(); await this.mainContextMenuHandler?.noAccess(); - await this.notificationsService.updateConnection(false); await this.systemService.clearPendingClipboard(); await this.processReloadService.startProcessReload(this.authService); } diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index b08f1c8b566c..8100ff3cffa2 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,12 +1,10 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; @@ -57,26 +55,29 @@ type ReceiveMessageOuter = { messageId?: number; // Should only have one of these. - message?: EncString; + message?: ReceiveMessage | EncString; sharedSecret?: string; }; type Callback = { - resolver: any; - rejecter: any; + resolver: (value?: unknown) => void; + rejecter: (reason?: any) => void; +}; + +type SecureChannel = { + privateKey: Uint8Array; + publicKey: Uint8Array; + sharedSecret?: SymmetricCryptoKey; + setupResolve: (value?: unknown) => void; }; export class NativeMessagingBackground { connected = false; - private connecting: boolean; - private port: browser.runtime.Port | chrome.runtime.Port; + private connecting: boolean = false; + private port?: browser.runtime.Port | chrome.runtime.Port; + private appId?: string; - private privateKey: Uint8Array = null; - private publicKey: Uint8Array = null; - private secureSetupResolve: any = null; - private sharedSecret: SymmetricCryptoKey; - private appId: string; - private validatingFingerprint: boolean; + private secureChannel?: SecureChannel; private messageId = 0; private callbacks = new Map(); @@ -108,11 +109,13 @@ export class NativeMessagingBackground { async connect() { this.logService.info("[Native Messaging IPC] Connecting to Bitwarden Desktop app..."); - this.appId = await this.appIdService.getAppId(); + const appId = await this.appIdService.getAppId(); + this.appId = appId; await this.biometricStateService.setFingerprintValidated(false); return new Promise((resolve, reject) => { - this.port = BrowserApi.connectNative("com.8bit.bitwarden"); + const port = BrowserApi.connectNative("com.8bit.bitwarden"); + this.port = port; this.connecting = true; @@ -131,7 +134,8 @@ export class NativeMessagingBackground { connectedCallback(); } - this.port.onMessage.addListener(async (message: ReceiveMessageOuter) => { + port.onMessage.addListener(async (messageRaw: unknown) => { + const message = messageRaw as ReceiveMessageOuter; switch (message.command) { case "connected": connectedCallback(); @@ -142,7 +146,7 @@ export class NativeMessagingBackground { reject(new Error("startDesktop")); } this.connected = false; - this.port.disconnect(); + port.disconnect(); // reject all for (const callback of this.callbacks.values()) { callback.rejecter("disconnected"); @@ -151,22 +155,31 @@ export class NativeMessagingBackground { break; case "setupEncryption": { // Ignore since it belongs to another device - if (message.appId !== this.appId) { + if (message.appId !== appId) { + return; + } + + if (message.sharedSecret == null) { + this.logService.info( + "[Native Messaging IPC] Unable to create secureChannel channel, no shared secret", + ); + return; + } + if (this.secureChannel == null) { + this.logService.info( + "[Native Messaging IPC] Unable to create secureChannel channel, no secureChannel communication setup", + ); return; } const encrypted = Utils.fromB64ToArray(message.sharedSecret); const decrypted = await this.cryptoFunctionService.rsaDecrypt( encrypted, - this.privateKey, + this.secureChannel.privateKey, HashAlgorithmForEncryption, ); - if (this.validatingFingerprint) { - this.validatingFingerprint = false; - await this.biometricStateService.setFingerprintValidated(true); - } - this.sharedSecret = new SymmetricCryptoKey(decrypted); + this.secureChannel.sharedSecret = new SymmetricCryptoKey(decrypted); this.logService.info("[Native Messaging IPC] Secure channel established"); if ("messageId" in message) { @@ -177,56 +190,70 @@ export class NativeMessagingBackground { this.isConnectedToOutdatedDesktopClient = true; } - this.secureSetupResolve(); + this.secureChannel.setupResolve(); break; } case "invalidateEncryption": // Ignore since it belongs to another device - if (message.appId !== this.appId) { + if (message.appId !== appId) { return; } this.logService.warning( "[Native Messaging IPC] Secure channel encountered an error; disconnecting and wiping keys...", ); - this.sharedSecret = null; - this.privateKey = null; + this.secureChannel = undefined; this.connected = false; - if (this.callbacks.has(message.messageId)) { - this.callbacks.get(message.messageId).rejecter({ - message: "invalidateEncryption", - }); + if (message.messageId != null) { + if (this.callbacks.has(message.messageId)) { + this.callbacks.get(message.messageId)?.rejecter({ + message: "invalidateEncryption", + }); + } } return; case "verifyFingerprint": { - if (this.sharedSecret == null) { - this.logService.info( - "[Native Messaging IPC] Desktop app requested trust verification by fingerprint.", - ); - this.validatingFingerprint = 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.showFingerprintDialog(); - } + this.logService.info("[Native Messaging IPC] Legacy app is requesting fingerprint"); + this.messagingService.send("showUpdateDesktopAppOrDisableFingerprintDialog", {}); + break; + } + case "verifyDesktopIPCFingerprint": { + this.logService.info( + "[Native Messaging IPC] Desktop app requested trust verification by fingerprint.", + ); + await this.showFingerprintDialog(); + break; + } + case "verifiedDesktopIPCFingerprint": { + await this.biometricStateService.setFingerprintValidated(true); + this.messagingService.send("hideNativeMessagingFingerprintDialog", {}); + break; + } + case "rejectedDesktopIPCFingerprint": { + this.messagingService.send("hideNativeMessagingFingerprintDialog", {}); break; } case "wrongUserId": - if (this.callbacks.has(message.messageId)) { - this.callbacks.get(message.messageId).rejecter({ - message: "wrongUserId", - }); + if (message.messageId != null) { + if (this.callbacks.has(message.messageId)) { + this.callbacks.get(message.messageId)?.rejecter({ + message: "wrongUserId", + }); + } } return; default: // Ignore since it belongs to another device - if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) { + if (!this.platformUtilsService.isSafari() && message.appId !== appId) { 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.onMessage(message.message); + if (message.message != null) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.onMessage(message.message); + } } }); @@ -235,16 +262,15 @@ export class NativeMessagingBackground { if (BrowserApi.isWebExtensionsApi) { error = p.error.message; } else { - error = chrome.runtime.lastError.message; + error = chrome.runtime.lastError?.message; } - this.sharedSecret = null; - this.privateKey = null; + this.secureChannel = undefined; this.connected = false; this.logService.error("NativeMessaging port disconnected because of error: " + error); - const reason = error != null ? "desktopIntegrationDisabled" : null; + const reason = error != null ? "desktopIntegrationDisabled" : undefined; reject(new Error(reason)); }); }); @@ -288,13 +314,13 @@ export class NativeMessagingBackground { ); const callback = this.callbacks.get(messageId); this.callbacks.delete(messageId); - callback.rejecter("errorConnecting"); + callback?.rejecter("errorConnecting"); } setTimeout(() => { if (this.callbacks.has(messageId)) { this.logService.info("[Native Messaging IPC] Message timed out and received no response"); - this.callbacks.get(messageId).rejecter({ + this.callbacks.get(messageId)!.rejecter({ message: "timeout", }); this.callbacks.delete(messageId); @@ -315,16 +341,19 @@ export class NativeMessagingBackground { if (this.platformUtilsService.isSafari()) { this.postMessage(message as any); } else { - this.postMessage({ appId: this.appId, message: await this.encryptMessage(message) }); + this.postMessage({ appId: this.appId!, message: await this.encryptMessage(message) }); } } async encryptMessage(message: Message) { - if (this.sharedSecret == null) { + if (this.secureChannel?.sharedSecret == null) { await this.secureCommunication(); } - return await this.encryptService.encrypt(JSON.stringify(message), this.sharedSecret); + return await this.encryptService.encrypt( + JSON.stringify(message), + this.secureChannel!.sharedSecret!, + ); } private postMessage(message: OuterMessage, messageId?: number) { @@ -341,7 +370,7 @@ export class NativeMessagingBackground { mac: message.message.mac, }; } - this.port.postMessage(msg); + this.port!.postMessage(msg); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { @@ -349,26 +378,30 @@ export class NativeMessagingBackground { "[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.", ); - this.sharedSecret = null; - this.privateKey = null; + this.secureChannel = undefined; this.connected = false; - if (this.callbacks.has(messageId)) { - this.callbacks.get(messageId).rejecter("invalidateEncryption"); + if (messageId != null && this.callbacks.has(messageId)) { + this.callbacks.get(messageId)!.rejecter("invalidateEncryption"); } } } private async onMessage(rawMessage: ReceiveMessage | EncString) { - let message = rawMessage as ReceiveMessage; + let message: ReceiveMessage; if (!this.platformUtilsService.isSafari()) { + if (this.secureChannel?.sharedSecret == null) { + return; + } message = JSON.parse( await this.encryptService.decryptToUtf8( rawMessage as EncString, - this.sharedSecret, + this.secureChannel.sharedSecret, "ipc-desktop-ipc-channel-key", ), ); + } else { + message = rawMessage as ReceiveMessage; } if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { @@ -385,15 +418,17 @@ export class NativeMessagingBackground { this.logService.info( `[Native Messaging IPC] Received legacy message of type ${message.command}`, ); - const messageId = this.callbacks.keys().next().value; - const resolver = this.callbacks.get(messageId); - this.callbacks.delete(messageId); - resolver.resolver(message); + const messageId: number | undefined = this.callbacks.keys().next().value; + if (messageId != null) { + const resolver = this.callbacks.get(messageId); + this.callbacks.delete(messageId); + resolver!.resolver(message); + } return; } if (this.callbacks.has(messageId)) { - this.callbacks.get(messageId).resolver(message); + this.callbacks.get(messageId)!.resolver(message); } else { this.logService.info("[Native Messaging IPC] Received message without a callback", message); } @@ -401,8 +436,6 @@ export class NativeMessagingBackground { private async secureCommunication() { 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. @@ -414,7 +447,13 @@ export class NativeMessagingBackground { messageId: this.messageId++, }); - return new Promise((resolve, reject) => (this.secureSetupResolve = resolve)); + return new Promise((resolve) => { + this.secureChannel = { + publicKey, + privateKey, + setupResolve: resolve, + }; + }); } private async sendUnencrypted(message: Message) { @@ -424,16 +463,19 @@ export class NativeMessagingBackground { message.timestamp = Date.now(); - this.postMessage({ appId: this.appId, message: message }); + this.postMessage({ appId: this.appId!, message: message }); } private async showFingerprintDialog() { + if (this.secureChannel?.publicKey == null) { + return; + } const fingerprint = await this.keyService.getFingerprint( - (await firstValueFrom(this.accountService.activeAccount$))?.id, - this.publicKey, + this.appId!, + this.secureChannel.publicKey, ); - this.messagingService.send("showNativeMessagingFinterprintDialog", { + this.messagingService.send("showNativeMessagingFingerprintDialog", { fingerprint: fingerprint, }); } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 38bb2ec50c99..2a7562930704 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -3,7 +3,6 @@ import { firstValueFrom, map, mergeMap } from "rxjs"; import { LockService } from "@bitwarden/auth/common"; -import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; @@ -16,13 +15,14 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { CipherType } from "@bitwarden/common/vault/enums"; import { BiometricsCommands } from "@bitwarden/key-management"; import { closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, + openTwoFactorAuthWebAuthnPopout, } from "../auth/popup/utils/auth-popout-window"; import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background"; import { AutofillService } from "../autofill/services/abstractions/autofill.service"; @@ -240,7 +240,6 @@ export default class RuntimeBackground { await closeUnlockPopout(); } - await this.notificationsService.updateConnection(msg.command === "loggedIn"); this.processReloadSerivce.cancelProcessReload(); if (item) { @@ -333,7 +332,7 @@ export default class RuntimeBackground { return; } - await openTwoFactorAuthPopout(msg); + await openTwoFactorAuthWebAuthnPopout(msg); break; } case "reloadPopup": diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.ts b/apps/browser/src/billing/popup/settings/premium-v2.component.ts index f658f71a2093..ff0e8efd6467 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.ts +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.ts @@ -4,12 +4,11 @@ import { CommonModule, CurrencyPipe, Location } from "@angular/common"; import { Component } from "@angular/core"; import { RouterModule } from "@angular/router"; +import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/billing/components/premium.component"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; @@ -20,6 +19,7 @@ import { DialogService, ItemModule, SectionComponent, + ToastService, } from "@bitwarden/components"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; @@ -50,24 +50,24 @@ export class PremiumV2Component extends BasePremiumComponent { i18nService: I18nService, platformUtilsService: PlatformUtilsService, apiService: ApiService, - configService: ConfigService, logService: LogService, private location: Location, private currencyPipe: CurrencyPipe, dialogService: DialogService, environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + toastService: ToastService, accountService: AccountService, ) { super( i18nService, platformUtilsService, apiService, - configService, logService, dialogService, environmentService, billingAccountProfileStateService, + toastService, accountService, ); diff --git a/apps/browser/src/services/families-policy.service.spec.ts b/apps/browser/src/billing/services/families-policy.service.spec.ts similarity index 80% rename from apps/browser/src/services/families-policy.service.spec.ts rename to apps/browser/src/billing/services/families-policy.service.spec.ts index 19291bcd8256..65a861038bf6 100644 --- a/apps/browser/src/services/families-policy.service.spec.ts +++ b/apps/browser/src/billing/services/families-policy.service.spec.ts @@ -6,6 +6,10 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { FamiliesPolicyService } from "./families-policy.service"; // Adjust the import as necessary @@ -13,16 +17,20 @@ describe("FamiliesPolicyService", () => { let service: FamiliesPolicyService; let organizationService: MockProxy; let policyService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); policyService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ providers: [ FamiliesPolicyService, { provide: OrganizationService, useValue: organizationService }, { provide: PolicyService, useValue: policyService }, + { provide: AccountService, useValue: accountService }, ], }); @@ -40,7 +48,7 @@ describe("FamiliesPolicyService", () => { jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true)); const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[]; - organizationService.getAll$.mockReturnValue(of(organizations)); + organizationService.organizations$.mockReturnValue(of(organizations)); const policies = [{ organizationId: "org1", enabled: true }] as Policy[]; policyService.getAll$.mockReturnValue(of(policies)); @@ -53,7 +61,7 @@ describe("FamiliesPolicyService", () => { jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true)); const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[]; - organizationService.getAll$.mockReturnValue(of(organizations)); + organizationService.organizations$.mockReturnValue(of(organizations)); const policies = [{ organizationId: "org1", enabled: false }] as Policy[]; policyService.getAll$.mockReturnValue(of(policies)); @@ -64,7 +72,7 @@ describe("FamiliesPolicyService", () => { it("should return true when there is exactly one enterprise organization that can manage sponsorships", async () => { const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[]; - organizationService.getAll$.mockReturnValue(of(organizations)); + organizationService.organizations$.mockReturnValue(of(organizations)); const result = await firstValueFrom(service.hasSingleEnterpriseOrg$()); expect(result).toBe(true); @@ -75,7 +83,7 @@ describe("FamiliesPolicyService", () => { { id: "org1", canManageSponsorships: true }, { id: "org2", canManageSponsorships: true }, ] as Organization[]; - organizationService.getAll$.mockReturnValue(of(organizations)); + organizationService.organizations$.mockReturnValue(of(organizations)); const result = await firstValueFrom(service.hasSingleEnterpriseOrg$()); expect(result).toBe(false); diff --git a/apps/browser/src/billing/services/families-policy.service.ts b/apps/browser/src/billing/services/families-policy.service.ts new file mode 100644 index 000000000000..755d3e845917 --- /dev/null +++ b/apps/browser/src/billing/services/families-policy.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from "@angular/core"; +import { map, Observable, of, switchMap } 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; + +@Injectable({ providedIn: "root" }) +export class FamiliesPolicyService { + constructor( + private policyService: PolicyService, + private organizationService: OrganizationService, + private accountService: AccountService, + ) {} + + hasSingleEnterpriseOrg$(): Observable { + // Retrieve all organizations the user is part of + return getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe( + map((organizations) => { + // Filter to only those organizations that can manage sponsorships + const sponsorshipOrgs = organizations.filter((org) => org.canManageSponsorships); + + // Check if there is exactly one organization that can manage sponsorships. + // This is important because users that are part of multiple organizations + // may always access free bitwarden family menu. We want to restrict access + // to the policy only when there is a single enterprise organization and the free family policy is turn. + return sponsorshipOrgs.length === 1; + }), + ), + ), + ); + } + + isFreeFamilyPolicyEnabled$(): Observable { + return this.hasSingleEnterpriseOrg$().pipe( + switchMap((hasSingleEnterpriseOrg) => { + if (!hasSingleEnterpriseOrg) { + return of(false); + } + return getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe( + map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id), + switchMap((enterpriseOrgId) => + this.policyService + .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId) + .pipe( + map( + (policies) => + policies.find((policy) => policy.organizationId === enterpriseOrgId) + ?.enabled ?? false, + ), + ), + ), + ), + ), + ); + }), + ); + } +} diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts index 92830c35aca6..faf5036d0f7e 100644 --- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts @@ -15,7 +15,7 @@ import { BiometricsStatus, BiometricStateService, } from "@bitwarden/key-management"; -import { UnlockOptions } from "@bitwarden/key-management/angular"; +import { UnlockOptions } from "@bitwarden/key-management-ui"; import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts index 711ae4e378fa..180336904e90 100644 --- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts @@ -13,7 +13,7 @@ import { BiometricsStatus, BiometricStateService, } from "@bitwarden/key-management"; -import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular"; +import { LockComponentService, UnlockOptions } from "@bitwarden/key-management-ui"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 86ea0eebbc81..a71de8506ea0 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": "2025.1.1", + "version": "2025.2.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -29,12 +29,6 @@ "matches": ["*://*/*", "file:///*"], "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" - }, - { - "all_frames": false, - "js": ["content/lp-fileless-importer.js"], - "matches": ["https://lastpass.com/export.php"], - "run_at": "document_start" } ], "background": { @@ -140,7 +134,6 @@ }, "web_accessible_resources": [ "content/fido2-page-script.js", - "content/lp-suppress-import-download.js", "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index ae7c888eb9bb..2de32e7c4a7d 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": "2025.1.1", + "version": "2025.2.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -30,12 +30,6 @@ "matches": ["*://*/*", "file:///*"], "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" - }, - { - "all_frames": false, - "js": ["content/lp-fileless-importer.js"], - "matches": ["https://lastpass.com/export.php"], - "run_at": "document_start" } ], "background": { @@ -66,7 +60,24 @@ "unlimitedStorage", "webNavigation", "webRequest", - "webRequestAuthProvider" + "webRequestAuthProvider", + "notifications" + ], + "__firefox__permissions": [ + "activeTab", + "alarms", + "clipboardRead", + "clipboardWrite", + "contextMenus", + "idle", + "scripting", + "storage", + "tabs", + "unlimitedStorage", + "webNavigation", + "webRequest", + "webRequestAuthProvider", + "notifications" ], "__safari__permissions": [ "activeTab", @@ -91,7 +102,7 @@ "host_permissions": ["https://*/*", "http://*/*"], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", - "sandbox": "sandbox allow-scripts; script-src 'self'" + "__chrome__sandbox": "sandbox allow-scripts; script-src 'self'" }, "sandbox": { "pages": [ 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 c1ac8823261c..43693ca223bd 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -45,7 +45,7 @@ class ExtensionContainerComponent {} - - + + @@ -306,6 +306,10 @@ export default { // Disable tests while we troubleshoot their flaky-ness disableSnapshot: true, }, + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-38889&t=k6OTDDPZOTtypRqo-11", + }, }, decorators: [ moduleMetadata({ 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 ba9a108a504a..94f0846a8529 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -13,13 +13,13 @@
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 index a717786ae525..69a0fa7d9d1e 100644 --- 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 @@ -133,6 +133,7 @@ export class PopupViewCacheService implements ViewCacheService { } private clearState() { + this._cache = {}; // clear local cache 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 index fbe94bece8c7..72cfd39cd62c 100644 --- 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 @@ -196,13 +196,15 @@ describe("popup view cache", () => { }); it("should clear on 2nd navigation", async () => { - await initServiceWithState({}); + await initServiceWithState({ temp: "state" }); await router.navigate(["a"]); expect(messageSenderMock.send).toHaveBeenCalledTimes(0); + expect(service["_cache"]).toEqual({ temp: "state" }); await router.navigate(["b"]); expect(messageSenderMock.send).toHaveBeenCalledWith(ClEAR_VIEW_CACHE_COMMAND, {}); + expect(service["_cache"]).toEqual({}); }); it("should ignore cached values when feature flag is off", async () => { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 4d6a403a18a0..1b93e33a94e6 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -1,6 +1,6 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; 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 900212ddefa4..b030ff46270f 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 @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Subject } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { 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 762380071b78..fe049c4f1db5 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 @@ -147,7 +147,9 @@ describe("Browser Utils Service", () => { describe("isViewOpen", () => { it("returns false if a heartbeat response is not received", async () => { - BrowserApi.sendMessageWithResponse = jest.fn().mockResolvedValueOnce(undefined); + chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => { + callback(undefined); + }); const isViewOpen = await browserPlatformUtilsService.isViewOpen(); @@ -155,16 +157,29 @@ describe("Browser Utils Service", () => { }); it("returns true if a heartbeat response is received", async () => { - BrowserApi.sendMessageWithResponse = jest - .fn() - .mockImplementationOnce((subscriber) => - Promise.resolve((subscriber === "checkVaultPopupHeartbeat") as any), - ); + chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => { + callback(message.command === "checkVaultPopupHeartbeat"); + }); const isViewOpen = await browserPlatformUtilsService.isViewOpen(); expect(isViewOpen).toBe(true); }); + + it("returns false if special error is sent", async () => { + chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => { + chrome.runtime.lastError = new Error( + "Could not establish connection. Receiving end does not exist.", + ); + callback(undefined); + }); + + const isViewOpen = await browserPlatformUtilsService.isViewOpen(); + + expect(isViewOpen).toBe(false); + + chrome.runtime.lastError = null; + }); }); describe("copyToClipboard", () => { @@ -228,6 +243,7 @@ describe("Browser Utils Service", () => { }); it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => { + BrowserApi.sendMessageWithResponse = jest.fn(); const text = "test"; offscreenDocumentService.offscreenApiSupported.mockReturnValue(true); getManifestVersionSpy.mockReturnValue(3); @@ -302,6 +318,7 @@ describe("Browser Utils Service", () => { }); it("reads the clipboard text using the offscreen document", async () => { + BrowserApi.sendMessageWithResponse = jest.fn(); offscreenDocumentService.offscreenApiSupported.mockReturnValue(true); getManifestVersionSpy.mockReturnValue(3); offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => 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 3679b2731e36..c9200ecc1a49 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 @@ -169,7 +169,28 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic // Query views on safari since chrome.runtime.sendMessage does not timeout and will hang. return BrowserApi.isPopupOpen(); } - return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat")); + + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ command: "checkVaultPopupHeartbeat" }, (response) => { + if (chrome.runtime.lastError != null) { + // This error means that nothing was there to listen to the message, + // meaning the view is not open. + if ( + chrome.runtime.lastError.message === + "Could not establish connection. Receiving end does not exist." + ) { + resolve(false); + return; + } + + // All unhandled errors still reject + reject(chrome.runtime.lastError); + return; + } + + resolve(Boolean(response)); + }); + }); } lockTimeout(): number { 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 index f6f5e45f0938..e739a3677f1a 100644 --- a/apps/browser/src/platform/services/popup-view-cache-background.service.ts +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -60,6 +60,11 @@ export class PopupViewCacheBackgroundService { ) .subscribe(); + this.messageListener + .messages$(ClEAR_VIEW_CACHE_COMMAND) + .pipe(concatMap(() => this.popupViewCacheState.update(() => null))) + .subscribe(); + merge( // on tab changed, excluding extension tabs fromChromeEvent(chrome.tabs.onActivated).pipe( diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts similarity index 55% rename from apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts rename to apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts index 0499f34a4aeb..ca41127407c4 100644 --- a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts +++ b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts @@ -1,11 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; -import type { BitwardenClient } from "@bitwarden/sdk-internal"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { BrowserApi } from "../../browser/browser-api"; +export type GlobalWithWasmInit = typeof globalThis & { + initSdk: () => void; +}; + // https://stackoverflow.com/a/47880734 const supported = (() => { try { @@ -17,9 +18,7 @@ const supported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + } catch { // ignore } return false; @@ -33,54 +32,42 @@ let loadingPromise: Promise | undefined; if (BrowserApi.isManifestVersion(3)) { if (supported) { // eslint-disable-next-line no-console - console.debug("WebAssembly is supported in this environment"); + console.info("WebAssembly is supported in this environment"); loadingPromise = import("./wasm"); } else { // eslint-disable-next-line no-console - console.debug("WebAssembly is not supported in this environment"); + console.info("WebAssembly is not supported in this environment"); loadingPromise = import("./fallback"); } } // Manifest v2 expects dynamic imports to prevent timing issues. -async function load() { +async function importModule(): Promise { if (BrowserApi.isManifestVersion(3)) { // Ensure we have loaded the module await loadingPromise; - return; - } - - if (supported) { + } else if (supported) { // eslint-disable-next-line no-console - console.debug("WebAssembly is supported in this environment"); + console.info("WebAssembly is supported in this environment"); await import("./wasm"); } else { // eslint-disable-next-line no-console - console.debug("WebAssembly is not supported in this environment"); + console.info("WebAssembly is not supported in this environment"); await import("./fallback"); } + + // the wasm and fallback imports mutate globalThis to add the initSdk function + return (globalThis as GlobalWithWasmInit).initSdk; } -/** - * SDK client factory with a js fallback for when WASM is not supported. - * - * Works both in popup and service worker. - */ -export class BrowserSdkClientFactory implements SdkClientFactory { - constructor(private logService: LogService) {} +export class BrowserSdkLoadService implements SdkLoadService { + constructor(readonly logService: LogService) {} - async createSdkClient( - ...args: ConstructorParameters - ): Promise { + async load(): Promise { const startTime = performance.now(); - await load(); - + await importModule().then((initSdk) => initSdk()); const endTime = performance.now(); - const instance = (globalThis as any).init_sdk(...args); - - this.logService.info("WASM SDK loaded in", Math.round(endTime - startTime), "ms"); - - return instance; + this.logService.info(`WASM SDK loaded in ${Math.round(endTime - startTime)}ms`); } } diff --git a/apps/browser/src/platform/services/sdk/fallback.ts b/apps/browser/src/platform/services/sdk/fallback.ts index 82d292fc9ee5..cee3598feda1 100644 --- a/apps/browser/src/platform/services/sdk/fallback.ts +++ b/apps/browser/src/platform/services/sdk/fallback.ts @@ -1,8 +1,8 @@ import * as sdk from "@bitwarden/sdk-internal"; import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"; -(globalThis as any).init_sdk = (...args: ConstructorParameters) => { - (sdk as any).init(wasm); +import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - return new sdk.BitwardenClient(...args); +(globalThis as GlobalWithWasmInit).initSdk = () => { + (sdk as any).init(wasm); }; diff --git a/apps/browser/src/platform/services/sdk/wasm.ts b/apps/browser/src/platform/services/sdk/wasm.ts index 1977a171e233..de2eeffd2944 100644 --- a/apps/browser/src/platform/services/sdk/wasm.ts +++ b/apps/browser/src/platform/services/sdk/wasm.ts @@ -1,8 +1,8 @@ import * as sdk from "@bitwarden/sdk-internal"; import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; -(globalThis as any).init_sdk = (...args: ConstructorParameters) => { - (sdk as any).init(wasm); +import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - return new sdk.BitwardenClient(...args); +(globalThis as GlobalWithWasmInit).initSdk = () => { + (sdk as any).init(wasm); }; diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index d55bebfa0c30..70a78ce548f7 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -1,17 +1,18 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { EnvironmentSelectorComponent, EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, + activeAuthGuard, redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, @@ -40,9 +41,11 @@ import { DevicesIcon, SsoComponent, TwoFactorTimeoutIcon, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { LockComponent } from "@bitwarden/key-management/angular"; +import { LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, @@ -174,12 +177,12 @@ const routes: Routes = [ component: ExtensionAnonLayoutWrapperComponent, children: [ { - path: "2fa-timeout", + path: "authentication-timeout", canActivate: [unauthGuardFn(unauthRouteOverrides)], children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, ], data: { @@ -232,6 +235,27 @@ const routes: Routes = [ ], }, ), + { + path: "device-verification", + component: ExtensionAnonLayoutWrapperComponent, + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [{ path: "", component: NewDeviceVerificationComponent }], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + }, { path: "set-password", component: SetPasswordComponent, @@ -413,7 +437,7 @@ const routes: Routes = [ data: { pageIcon: DevicesIcon, pageTitle: { - key: "loginInitiated", + key: "logInRequestSent", }, pageSubtitle: { key: "aNotificationWasSentToYourDevice", @@ -559,7 +583,7 @@ const routes: Routes = [ children: [ { path: "signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { elevation: 1, pageIcon: RegistrationUserAddIcon, @@ -585,7 +609,7 @@ const routes: Routes = [ }, { path: "finish-signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationLockAltIcon, elevation: 1, @@ -635,7 +659,6 @@ const routes: Routes = [ children: [ { path: "set-password-jit", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { pageTitle: { diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 9d4835889b9f..8147524d2f3e 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -128,11 +128,18 @@ export class AppComponent implements OnInit, OnDestroy { // 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.showDialog(msg); - } else if (msg.command === "showNativeMessagingFinterprintDialog") { + } else if (msg.command === "showNativeMessagingFingerprintDialog") { // TODO: Should be refactored to live in another service. // 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.showNativeMessagingFingerprintDialog(msg); + } else if (msg.command === "showUpdateDesktopAppOrDisableFingerprintDialog") { + // TODO: Should be refactored to live in another service. + await this.showDialog({ + title: this.i18nService.t("updateDesktopAppOrDisableFingerprintDialogTitle"), + content: this.i18nService.t("updateDesktopAppOrDisableFingerprintDialogMessage"), + type: "warning", + }); } else if (msg.command === "showToast") { this.toastService._showToast(msg); } else if (msg.command === "reloadProcess") { diff --git a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts index c860ef1e3422..2a5a56e7dfcb 100644 --- a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts +++ b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts @@ -1,7 +1,9 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { filter, Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { MessageListener } from "@bitwarden/common/platform/messaging"; import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; export type DesktopSyncVerificationDialogParams = { @@ -13,8 +15,30 @@ export type DesktopSyncVerificationDialogParams = { standalone: true, imports: [JslibModule, ButtonModule, DialogModule], }) -export class DesktopSyncVerificationDialogComponent { - constructor(@Inject(DIALOG_DATA) protected params: DesktopSyncVerificationDialogParams) {} +export class DesktopSyncVerificationDialogComponent implements OnDestroy, OnInit { + private destroy$ = new Subject(); + + constructor( + @Inject(DIALOG_DATA) protected params: DesktopSyncVerificationDialogParams, + private dialogRef: DialogRef, + private messageListener: MessageListener, + ) {} + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngOnInit(): void { + this.messageListener.allMessages$ + .pipe( + filter((m) => m.command === "hideNativeMessagingFingerprintDialog"), + takeUntil(this.destroy$), + ) + .subscribe(() => { + this.dialogRef.close(); + }); + } static open(dialogService: DialogService, data: DesktopSyncVerificationDialogParams) { return dialogService.open(DesktopSyncVerificationDialogComponent, { diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 24661438495f..069ebf4020d7 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -6,6 +6,7 @@ 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 { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -22,11 +23,13 @@ export class InitService { private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); this.twoFactorService.init(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 24d82ab8b67d..257497fb13da 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,8 +31,8 @@ import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarde import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.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 { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { AccountService, AccountService as AccountServiceAbstraction, @@ -55,13 +55,13 @@ 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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; 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 { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -70,6 +70,7 @@ 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 { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, @@ -82,6 +83,7 @@ import { flagEnabled } from "@bitwarden/common/platform/misc/flags"; 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 { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -114,7 +116,7 @@ import { BiometricsService, DefaultKeyService, } from "@bitwarden/key-management"; -import { LockComponentService } from "@bitwarden/key-management/angular"; +import { LockComponentService } from "@bitwarden/key-management-ui"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; @@ -144,7 +146,7 @@ import BrowserMemoryStorageService from "../../platform/services/browser-memory- import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; -import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk-client-factory"; +import { BrowserSdkLoadService } from "../../platform/services/sdk/browser-sdk-load.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"; @@ -369,7 +371,7 @@ const safeProviders: SafeProvider[] = [ provide: VaultFilterService, useClass: VaultFilterService, deps: [ - OrganizationService, + DefaultOrganizationService, FolderServiceAbstraction, CipherService, CollectionService, @@ -566,11 +568,16 @@ const safeProviders: SafeProvider[] = [ deps: [MessageSender, MessageListener], }), safeProvider({ - provide: SdkClientFactory, - useFactory: (logService: LogService) => - flagEnabled("sdk") ? new BrowserSdkClientFactory(logService) : new NoopSdkClientFactory(), + provide: SdkLoadService, + useClass: BrowserSdkLoadService, deps: [LogService], }), + safeProvider({ + provide: SdkClientFactory, + useFactory: () => + flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(), + deps: [], + }), safeProvider({ provide: LoginEmailService, useClass: LoginEmailService, diff --git a/apps/browser/src/services/families-policy.service.ts b/apps/browser/src/services/families-policy.service.ts deleted file mode 100644 index 426f39dcfd05..000000000000 --- a/apps/browser/src/services/families-policy.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Injectable } from "@angular/core"; -import { map, Observable, of, switchMap } 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"; - -@Injectable({ providedIn: "root" }) -export class FamiliesPolicyService { - constructor( - private policyService: PolicyService, - private organizationService: OrganizationService, - ) {} - - hasSingleEnterpriseOrg$(): Observable { - // Retrieve all organizations the user is part of - return this.organizationService.getAll$().pipe( - map((organizations) => { - // Filter to only those organizations that can manage sponsorships - const sponsorshipOrgs = organizations.filter((org) => org.canManageSponsorships); - - // Check if there is exactly one organization that can manage sponsorships. - // This is important because users that are part of multiple organizations - // may always access free bitwarden family menu. We want to restrict access - // to the policy only when there is a single enterprise organization and the free family policy is turn. - return sponsorshipOrgs.length === 1; - }), - ); - } - - isFreeFamilyPolicyEnabled$(): Observable { - return this.hasSingleEnterpriseOrg$().pipe( - switchMap((hasSingleEnterpriseOrg) => { - if (!hasSingleEnterpriseOrg) { - return of(false); - } - return this.organizationService.getAll$().pipe( - map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id), - switchMap((enterpriseOrgId) => - this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy) - .pipe( - map( - (policies) => - policies.find((policy) => policy.organizationId === enterpriseOrgId)?.enabled ?? - false, - ), - ), - ), - ); - }), - ); - } -} diff --git a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts deleted file mode 100644 index 2ade5bf7672a..000000000000 --- a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums"; - -type FilelessImportPortMessage = { - command?: string; - importType?: FilelessImportTypeKeys; - data?: string; -}; - -type FilelessImportPortMessageHandlerParams = { - message: FilelessImportPortMessage; - port: chrome.runtime.Port; -}; - -type ImportNotificationMessageHandlers = { - [key: string]: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; - cancelFilelessImport: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; -}; - -type LpImporterMessageHandlers = { - [key: string]: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; - displayLpImportNotification: ({ port }: { port: chrome.runtime.Port }) => void; - startLpImport: ({ message }: { message: FilelessImportPortMessage }) => void; -}; - -interface FilelessImporterBackground { - init(): void; -} - -export { - FilelessImportPortMessage, - ImportNotificationMessageHandlers, - LpImporterMessageHandlers, - FilelessImporterBackground, -}; diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts deleted file mode 100644 index 409fac9790f1..000000000000 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; - -import NotificationBackground from "../../autofill/background/notification.background"; -import { createPortSpyMock } from "../../autofill/spec/autofill-mocks"; -import { - flushPromises, - sendPortMessage, - triggerRuntimeOnConnectEvent, -} from "../../autofill/spec/testing-utils"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; -import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums"; - -import FilelessImporterBackground from "./fileless-importer.background"; - -describe("FilelessImporterBackground ", () => { - let filelessImporterBackground: FilelessImporterBackground; - const configService = mock(); - const domainSettingsService = mock(); - const authService = mock(); - const policyService = mock(); - const notificationBackground = mock(); - const importService = mock(); - const syncService = mock(); - const platformUtilsService = mock(); - const logService = mock(); - let scriptInjectorService: BrowserScriptInjectorService; - let tabMock: chrome.tabs.Tab; - - beforeEach(() => { - domainSettingsService.blockedInteractionsUris$ = of({}); - policyService.policyAppliesToActiveUser$.mockImplementation(() => of(true)); - scriptInjectorService = new BrowserScriptInjectorService( - domainSettingsService, - platformUtilsService, - logService, - ); - filelessImporterBackground = new FilelessImporterBackground( - configService, - authService, - policyService, - notificationBackground, - importService, - syncService, - scriptInjectorService, - ); - filelessImporterBackground.init(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("init", () => { - it("sets up the port message listeners on initialization of the class", () => { - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function)); - }); - }); - - describe("handle ports onConnect", () => { - let lpImporterPort: chrome.runtime.Port; - let manifestVersionSpy: jest.SpyInstance; - let executeScriptInTabSpy: jest.SpyInstance; - - beforeEach(() => { - lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); - tabMock = lpImporterPort.sender.tab; - jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock); - manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null); - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - jest.spyOn(filelessImporterBackground as any, "removeIndividualVault"); - }); - - it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => { - const port = createPortSpyMock("some-other-port"); - - triggerRuntimeOnConnectEvent(port); - await flushPromises(); - - expect(port.postMessage).not.toHaveBeenCalled(); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the user's auth status is not unlocked", async () => { - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Locked); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => { - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the feature flag is turned off", async () => { - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(false); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is enabled", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }); - }); - - it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - manifestVersionSpy.mockReturnValue(3); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(executeScriptInTabSpy).toHaveBeenCalledWith( - lpImporterPort.sender.tab.id, - { file: "content/lp-suppress-import-download.js", runAt: "document_start", frameId: 0 }, - { world: "MAIN" }, - ); - }); - - it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - manifestVersionSpy.mockReturnValue(2); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(executeScriptInTabSpy).toHaveBeenCalledWith(lpImporterPort.sender.tab.id, { - file: "content/lp-suppress-import-download-script-append-mv2.js", - runAt: "document_start", - frameId: 0, - }); - }); - }); - - describe("port messages", () => { - let notificationPort: chrome.runtime.Port; - let lpImporterPort: chrome.runtime.Port; - - beforeEach(async () => { - policyService.policyAppliesToActiveUser$.mockImplementation(() => of(false)); - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - - triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar)); - triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter)); - await flushPromises(); - notificationPort = filelessImporterBackground["importNotificationsPort"]; - lpImporterPort = filelessImporterBackground["lpImporterPort"]; - }); - - it("skips handling a message if a message handler is not associated with the port message command", () => { - sendPortMessage(notificationPort, { command: "commandNotFound" }); - - expect(chrome.tabs.sendMessage).not.toHaveBeenCalled(); - }); - - describe("import notification port messages", () => { - describe("startFilelessImport", () => { - it("sends a message to start the LastPass fileless import within the content script", () => { - sendPortMessage(notificationPort, { - command: "startFilelessImport", - importType: FilelessImportType.LP, - }); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "startLpFilelessImport", - }); - }); - }); - - describe("cancelFilelessImport", () => { - it("sends a message to close the notification bar", async () => { - sendPortMessage(notificationPort, { command: "cancelFilelessImport" }); - - expect(chrome.tabs.sendMessage).toHaveBeenCalledWith( - notificationPort.sender.tab.id, - { - command: "closeNotificationBar", - }, - null, - expect.anything(), - ); - expect(lpImporterPort.postMessage).not.toHaveBeenCalledWith({ - command: "triggerCsvDownload", - }); - }); - - it("sends a message to trigger a download of the LP importer CSV", () => { - sendPortMessage(notificationPort, { - command: "cancelFilelessImport", - importType: FilelessImportType.LP, - }); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "triggerCsvDownload", - }); - expect(lpImporterPort.disconnect).toHaveBeenCalled(); - }); - }); - }); - - describe("lp importer port messages", () => { - describe("displayLpImportNotification", () => { - it("creates a request fileless import notification", async () => { - jest.spyOn(filelessImporterBackground["notificationBackground"], "requestFilelessImport"); - - sendPortMessage(lpImporterPort, { - command: "displayLpImportNotification", - }); - await flushPromises(); - - expect( - filelessImporterBackground["notificationBackground"].requestFilelessImport, - ).toHaveBeenCalledWith(lpImporterPort.sender.tab, FilelessImportType.LP); - }); - }); - - describe("startLpImport", () => { - it("ignores the message if the message does not contain data", () => { - sendPortMessage(lpImporterPort, { - command: "startLpImport", - }); - - expect(filelessImporterBackground["importService"].import).not.toHaveBeenCalled(); - }); - - it("triggers the import of the LastPass vault", async () => { - const data = "url,username,password"; - const importer = mock(); - jest - .spyOn(filelessImporterBackground["importService"], "getImporter") - .mockReturnValue(importer); - jest.spyOn(filelessImporterBackground["importService"], "import").mockResolvedValue( - mock({ - success: true, - }), - ); - jest.spyOn(filelessImporterBackground["syncService"], "fullSync"); - - sendPortMessage(lpImporterPort, { - command: "startLpImport", - data, - }); - await flushPromises(); - - expect(filelessImporterBackground["importService"].import).toHaveBeenCalledWith( - importer, - data, - null, - null, - false, - ); - expect( - filelessImporterBackground["importNotificationsPort"].postMessage, - ).toHaveBeenCalledWith({ command: "filelessImportCompleted" }); - expect(filelessImporterBackground["syncService"].fullSync).toHaveBeenCalledWith(true); - }); - - it("posts a failed message if the import fails", async () => { - const data = "url,username,password"; - const importer = mock(); - jest - .spyOn(filelessImporterBackground["importService"], "getImporter") - .mockReturnValue(importer); - jest - .spyOn(filelessImporterBackground["importService"], "import") - .mockImplementation(() => { - throw new Error("error"); - }); - jest.spyOn(filelessImporterBackground["syncService"], "fullSync"); - - sendPortMessage(lpImporterPort, { - command: "startLpImport", - data, - }); - await flushPromises(); - - expect( - filelessImporterBackground["importNotificationsPort"].postMessage, - ).toHaveBeenCalledWith({ command: "filelessImportFailed" }); - }); - }); - }); - }); - - describe("handleImporterPortDisconnect", () => { - it("resets the port properties to null", () => { - const lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); - const notificationPort = createPortSpyMock(FilelessImportPort.NotificationBar); - filelessImporterBackground["lpImporterPort"] = lpImporterPort; - filelessImporterBackground["importNotificationsPort"] = notificationPort; - - filelessImporterBackground["handleImporterPortDisconnect"](lpImporterPort); - - expect(filelessImporterBackground["lpImporterPort"]).toBeNull(); - expect(filelessImporterBackground["importNotificationsPort"]).not.toBeNull(); - - filelessImporterBackground["handleImporterPortDisconnect"](notificationPort); - - expect(filelessImporterBackground["importNotificationsPort"]).toBeNull(); - }); - }); -}); diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts deleted file mode 100644 index 21d597ec8ae7..000000000000 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ /dev/null @@ -1,265 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -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 { 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ImportServiceAbstraction } from "@bitwarden/importer/core"; - -import NotificationBackground from "../../autofill/background/notification.background"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; -import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts"; -import { - FilelessImportPort, - FilelessImportType, - FilelessImportTypeKeys, -} from "../enums/fileless-import.enums"; - -import { - ImportNotificationMessageHandlers, - LpImporterMessageHandlers, - FilelessImporterBackground as FilelessImporterBackgroundInterface, - FilelessImportPortMessage, -} from "./abstractions/fileless-importer.background"; - -class FilelessImporterBackground implements FilelessImporterBackgroundInterface { - private static readonly filelessImporterPortNames: Set = new Set([ - FilelessImportPort.LpImporter, - FilelessImportPort.NotificationBar, - ]); - private importNotificationsPort: chrome.runtime.Port; - private lpImporterPort: chrome.runtime.Port; - private readonly importNotificationsPortMessageHandlers: ImportNotificationMessageHandlers = { - startFilelessImport: ({ message }) => this.startFilelessImport(message.importType), - cancelFilelessImport: ({ message, port }) => - this.cancelFilelessImport(message.importType, port.sender), - }; - private readonly lpImporterPortMessageHandlers: LpImporterMessageHandlers = { - displayLpImportNotification: ({ port }) => - this.displayFilelessImportNotification(port.sender.tab, FilelessImportType.LP), - startLpImport: ({ message }) => this.triggerLpImport(message.data), - }; - - /** - * Creates a new instance of the fileless importer background logic. - * - * @param configService - Identifies if the feature flag is enabled. - * @param authService - Verifies if the auth status of the user. - * @param policyService - Identifies if the user account has a policy that disables personal ownership. - * @param notificationBackground - Used to inject the notification bar into the tab. - * @param importService - Used to import the export data into the vault. - * @param syncService - Used to trigger a full sync after the import is completed. - * @param scriptInjectorService - Used to inject content scripts that initialize the import process - */ - constructor( - private configService: ConfigService, - private authService: AuthService, - private policyService: PolicyService, - private notificationBackground: NotificationBackground, - private importService: ImportServiceAbstraction, - private syncService: SyncService, - private scriptInjectorService: ScriptInjectorService, - ) {} - - /** - * Initializes the fileless importer background logic. - */ - init() { - this.setupPortMessageListeners(); - } - - /** - * Starts an import of the export data pulled from the tab. - * - * @param importType - The type of import to start. Identifies the used content script. - */ - private startFilelessImport(importType: FilelessImportTypeKeys) { - if (importType === FilelessImportType.LP) { - this.lpImporterPort?.postMessage({ command: "startLpFilelessImport" }); - } - } - - /** - * Cancels an import of the export data pulled from the tab. This closes any - * existing notifications that are present in the tab, and triggers importer - * specific behavior based on the import type. - * - * @param importType - The type of import to cancel. Identifies the used content script. - * @param sender - The sender of the message. - */ - private async cancelFilelessImport( - importType: FilelessImportTypeKeys, - sender: chrome.runtime.MessageSender, - ) { - if (importType === FilelessImportType.LP) { - this.triggerLpImporterCsvDownload(); - } - - await BrowserApi.tabSendMessage(sender.tab, { command: "closeNotificationBar" }); - } - - /** - * Injects the notification bar into the passed tab. - * - * @param tab - * @param importType - */ - private async displayFilelessImportNotification(tab: chrome.tabs.Tab, importType: string) { - await this.notificationBackground.requestFilelessImport(tab, importType); - } - - /** - * Triggers the download of the CSV file from the LP importer. This is triggered - * when the user opts to not save the export to Bitwarden within the notification bar. - */ - private triggerLpImporterCsvDownload() { - this.lpImporterPort?.postMessage({ command: "triggerCsvDownload" }); - this.lpImporterPort?.disconnect(); - } - - /** - * Completes the import process for the LP importer. This is triggered when the - * user opts to save the export to Bitwarden within the notification bar. - * - * @param data - The export data to import. - * @param sender - The sender of the message. - */ - private async triggerLpImport(data: string) { - if (!data) { - return; - } - - const promptForPassword_callback = async () => ""; - const importer = this.importService.getImporter( - "lastpasscsv", - promptForPassword_callback, - null, - ); - - try { - const result = await this.importService.import(importer, data, null, null, false); - if (result.success) { - this.importNotificationsPort?.postMessage({ command: "filelessImportCompleted" }); - await this.syncService.fullSync(true); - } - } catch (error) { - this.importNotificationsPort?.postMessage({ - command: "filelessImportFailed", - importErrorMessage: Object.values(error).length - ? error - : chrome.i18n.getMessage("importNetworkError"), - }); - } - } - - /** - * Identifies if the user account has a policy that disables personal ownership. - */ - private async removeIndividualVault(): Promise { - return await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), - ); - } - - /** - * Sets up onConnect listeners for the extension. - */ - private setupPortMessageListeners() { - chrome.runtime.onConnect.addListener(this.handlePortOnConnect); - } - - /** - * Handles connections from content scripts that affect the fileless importer behavior. - * Is used to facilitate the passing of data and user actions to enact the import - * of web content to the Bitwarden vault. Along with this, a check is made to ensure - * that the feature flag is enabled and the user is authenticated. - */ - private handlePortOnConnect = async (port: chrome.runtime.Port) => { - if (!FilelessImporterBackground.filelessImporterPortNames.has(port.name)) { - return; - } - - const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.BrowserFilelessImport, - ); - const userAuthStatus = await this.authService.getAuthStatus(); - const removeIndividualVault = await this.removeIndividualVault(); - const filelessImportEnabled = - filelessImportFeatureFlagEnabled && - userAuthStatus === AuthenticationStatus.Unlocked && - !removeIndividualVault; - port.postMessage({ command: "verifyFeatureFlag", filelessImportEnabled }); - - if (!filelessImportEnabled) { - return; - } - - port.onMessage.addListener(this.handleImporterPortMessage); - port.onDisconnect.addListener(this.handleImporterPortDisconnect); - - switch (port.name) { - case FilelessImportPort.LpImporter: - this.lpImporterPort = port; - await this.scriptInjectorService.inject({ - tabId: port.sender.tab.id, - injectDetails: { runAt: "document_start" }, - mv2Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, - mv3Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3, - }); - break; - case FilelessImportPort.NotificationBar: - this.importNotificationsPort = port; - break; - } - }; - - /** - * Handles messages that are sent from fileless importer content scripts. - * @param message - The message that was sent. - * @param port - The port that the message was sent from. - */ - private handleImporterPortMessage = ( - message: FilelessImportPortMessage, - port: chrome.runtime.Port, - ) => { - let handler: CallableFunction | undefined; - - switch (port.name) { - case FilelessImportPort.LpImporter: - handler = this.lpImporterPortMessageHandlers[message.command]; - break; - case FilelessImportPort.NotificationBar: - handler = this.importNotificationsPortMessageHandlers[message.command]; - break; - } - - if (!handler) { - return; - } - - handler({ message, port }); - }; - - /** - * Handles disconnections from fileless importer content scripts. - * @param port - The port that was disconnected. - */ - private handleImporterPortDisconnect = (port: chrome.runtime.Port) => { - switch (port.name) { - case FilelessImportPort.LpImporter: - this.lpImporterPort = null; - break; - case FilelessImportPort.NotificationBar: - this.importNotificationsPort = null; - break; - } - }; -} - -export default FilelessImporterBackground; diff --git a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts deleted file mode 100644 index 898ee1205aba..000000000000 --- a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - Mv2ScriptInjectionDetails, - Mv3ScriptInjectionDetails, -} from "../../platform/services/abstractions/script-injector.service"; - -type FilelessImporterInjectedScriptsConfigurations = { - LpSuppressImportDownload: { - mv2: Mv2ScriptInjectionDetails; - mv3: Mv3ScriptInjectionDetails; - }; -}; - -const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConfigurations = { - LpSuppressImportDownload: { - mv2: { - file: "content/lp-suppress-import-download-script-append-mv2.js", - }, - mv3: { - file: "content/lp-suppress-import-download.js", - world: "MAIN", - }, - }, -} as const; - -export { FilelessImporterInjectedScriptsConfig }; diff --git a/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts b/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts deleted file mode 100644 index 018ea2c8d948..000000000000 --- a/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts +++ /dev/null @@ -1,25 +0,0 @@ -type LpFilelessImporterMessage = { - command?: string; - data?: string; - filelessImportEnabled?: boolean; -}; - -type LpFilelessImporterMessageHandlerParams = { - message: LpFilelessImporterMessage; - port: chrome.runtime.Port; -}; - -type LpFilelessImporterMessageHandlers = { - [key: string]: ({ message, port }: LpFilelessImporterMessageHandlerParams) => void; - verifyFeatureFlag: ({ message }: { message: LpFilelessImporterMessage }) => void; - triggerCsvDownload: () => void; - startLpFilelessImport: () => void; -}; - -interface LpFilelessImporter { - init(): void; - handleFeatureFlagVerification(message: LpFilelessImporterMessage): void; - triggerCsvDownload(): void; -} - -export { LpFilelessImporterMessage, LpFilelessImporterMessageHandlers, LpFilelessImporter }; diff --git a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts deleted file mode 100644 index 21fa44b8d3fa..000000000000 --- a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { createPortSpyMock } from "../../autofill/spec/autofill-mocks"; -import { sendPortMessage } from "../../autofill/spec/testing-utils"; -import { FilelessImportPort } from "../enums/fileless-import.enums"; - -import { LpFilelessImporter } from "./abstractions/lp-fileless-importer"; - -describe("LpFilelessImporter", () => { - let lpFilelessImporter: LpFilelessImporter & { [key: string]: any }; - const portSpy: chrome.runtime.Port = createPortSpyMock(FilelessImportPort.LpImporter); - chrome.runtime.connect = jest.fn(() => portSpy); - - beforeEach(() => { - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-require-imports - require("./lp-fileless-importer"); - lpFilelessImporter = (globalThis as any).lpFilelessImporter; - }); - - afterEach(() => { - (globalThis as any).lpFilelessImporter = undefined; - jest.clearAllMocks(); - jest.resetModules(); - Object.defineProperty(document, "readyState", { - value: "complete", - writable: true, - }); - }); - - describe("init", () => { - it("sets up the port connection with the background script", () => { - lpFilelessImporter.init(); - - expect(chrome.runtime.connect).toHaveBeenCalledWith({ - name: FilelessImportPort.LpImporter, - }); - }); - }); - - describe("handleFeatureFlagVerification", () => { - it("disconnects the message port when the fileless import feature is disabled", () => { - lpFilelessImporter.handleFeatureFlagVerification({ filelessImportEnabled: false }); - - expect(portSpy.disconnect).toHaveBeenCalled(); - }); - - it("sets up an event listener for DOMContentLoaded that triggers the importer when the document ready state is `loading`", () => { - Object.defineProperty(document, "readyState", { - value: "loading", - writable: true, - }); - const message = { - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }; - jest.spyOn(document, "addEventListener"); - - lpFilelessImporter.handleFeatureFlagVerification(message); - - expect(document.addEventListener).toHaveBeenCalledWith( - "DOMContentLoaded", - (lpFilelessImporter as any).loadImporter, - ); - }); - - it("sets up a mutation observer to watch the document body for injection of the export content", () => { - const message = { - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }; - jest.spyOn(document, "addEventListener"); - jest.spyOn(window, "MutationObserver").mockImplementationOnce(() => mock()); - - lpFilelessImporter.handleFeatureFlagVerification(message); - - expect(window.MutationObserver).toHaveBeenCalledWith( - (lpFilelessImporter as any).handleMutation, - ); - expect((lpFilelessImporter as any).mutationObserver.observe).toHaveBeenCalledWith( - document.body, - { childList: true, subtree: true }, - ); - }); - }); - - describe("triggerCsvDownload", () => { - it("posts a window message that triggers the download of the LastPass export", () => { - jest.spyOn(globalThis, "postMessage"); - - lpFilelessImporter.triggerCsvDownload(); - - expect(globalThis.postMessage).toHaveBeenCalledWith( - { command: "triggerCsvDownload" }, - "https://lastpass.com", - ); - }); - }); - - describe("handleMutation", () => { - beforeEach(() => { - lpFilelessImporter["mutationObserver"] = mock({ disconnect: jest.fn() }); - jest.spyOn(portSpy, "postMessage"); - }); - - it("ignores mutations that contain empty records", () => { - lpFilelessImporter["handleMutation"]([]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations that have no added nodes in the mutation", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations that have no added nodes with a tagname of `pre`", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [{ nodeName: "div" }] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations where the found `pre` element does not contain any textContent", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [{ nodeName: "pre" }] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations where the found `pre` element does not contain the expected header content", () => { - lpFilelessImporter["handleMutation"]([ - { addedNodes: [{ nodeName: "pre", textContent: "some other content" }] }, - ]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("will store the export data, display the import notification, and disconnect the mutation observer when the export data is appended", () => { - const observerDisconnectSpy = jest.spyOn( - lpFilelessImporter["mutationObserver"], - "disconnect", - ); - - lpFilelessImporter["handleMutation"]([ - { addedNodes: [{ nodeName: "pre", textContent: "url,username,password" }] }, - ]); - - expect(lpFilelessImporter["exportData"]).toEqual("url,username,password"); - expect(portSpy.postMessage).toHaveBeenCalledWith({ command: "displayLpImportNotification" }); - expect(observerDisconnectSpy).toHaveBeenCalled(); - }); - }); - - describe("handlePortMessage", () => { - it("ignores messages that are not registered with the portMessageHandlers", () => { - const message = { command: "unknownCommand" }; - jest.spyOn(lpFilelessImporter, "handleFeatureFlagVerification"); - jest.spyOn(lpFilelessImporter, "triggerCsvDownload"); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.handleFeatureFlagVerification).not.toHaveBeenCalled(); - expect(lpFilelessImporter.triggerCsvDownload).not.toHaveBeenCalled(); - }); - - it("handles the port message that verifies the fileless import feature flag", () => { - const message = { command: "verifyFeatureFlag", filelessImportEnabled: true }; - jest.spyOn(lpFilelessImporter, "handleFeatureFlagVerification").mockImplementation(); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.handleFeatureFlagVerification).toHaveBeenCalledWith(message); - }); - - it("handles the port message that triggers the LastPass csv download", () => { - const message = { command: "triggerCsvDownload" }; - jest.spyOn(lpFilelessImporter, "triggerCsvDownload"); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.triggerCsvDownload).toHaveBeenCalled(); - }); - - describe("handles the port message that triggers the LastPass fileless import", () => { - beforeEach(() => { - jest.spyOn(lpFilelessImporter as any, "postPortMessage"); - }); - - it("skips the import of the export data is not populated", () => { - const message = { command: "startLpFilelessImport" }; - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.postPortMessage).not.toHaveBeenCalled(); - }); - - it("starts the last pass fileless import", () => { - const message = { command: "startLpFilelessImport" }; - const exportData = "url,username,password"; - lpFilelessImporter["exportData"] = exportData; - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.postPortMessage).toHaveBeenCalledWith({ - command: "startLpImport", - data: exportData, - }); - }); - }); - }); -}); diff --git a/apps/browser/src/tools/content/lp-fileless-importer.ts b/apps/browser/src/tools/content/lp-fileless-importer.ts deleted file mode 100644 index 497a499b3379..000000000000 --- a/apps/browser/src/tools/content/lp-fileless-importer.ts +++ /dev/null @@ -1,158 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { FilelessImportPort } from "../enums/fileless-import.enums"; - -import { - LpFilelessImporter as LpFilelessImporterInterface, - LpFilelessImporterMessage, - LpFilelessImporterMessageHandlers, -} from "./abstractions/lp-fileless-importer"; - -class LpFilelessImporter implements LpFilelessImporterInterface { - private exportData: string; - private messagePort: chrome.runtime.Port; - private mutationObserver: MutationObserver; - private readonly portMessageHandlers: LpFilelessImporterMessageHandlers = { - verifyFeatureFlag: ({ message }) => this.handleFeatureFlagVerification(message), - triggerCsvDownload: () => this.triggerCsvDownload(), - startLpFilelessImport: () => this.startLpImport(), - }; - - /** - * Initializes the LP fileless importer. - */ - init() { - this.setupMessagePort(); - } - - /** - * Enacts behavior based on the feature flag verification message. If the feature flag is - * not enabled, the message port is disconnected. If the feature flag is enabled, the - * download of the CSV file is suppressed. - * - * @param message - The port message, contains the feature flag indicator. - */ - handleFeatureFlagVerification(message: LpFilelessImporterMessage) { - if (!message.filelessImportEnabled) { - this.messagePort?.disconnect(); - return; - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", this.loadImporter); - return; - } - - this.loadImporter(); - } - - /** - * Posts a message to the LP importer to trigger the download of the CSV file. - */ - triggerCsvDownload() { - this.postWindowMessage({ command: "triggerCsvDownload" }); - } - - /** - * Initializes the importing mechanism used to import the CSV file into Bitwarden. - * This is done by observing the DOM for the addition of the LP importer element. - */ - private loadImporter = () => { - this.mutationObserver = new MutationObserver(this.handleMutation); - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true, - }); - }; - - /** - * Handles mutations that are observed by the mutation observer. When the exported data - * element is added to the DOM, the export data is extracted and the import prompt is - * displayed. - * - * @param mutations - The mutations that were observed. - */ - private handleMutation = (mutations: MutationRecord[]) => { - let textContent: string; - for (let index = 0; index < mutations?.length; index++) { - const mutation: MutationRecord = mutations[index]; - - textContent = Array.from(mutation.addedNodes) - .filter((node) => node.nodeName.toLowerCase() === "pre") - .map((node) => (node as HTMLPreElement).textContent?.trim()) - .find((text) => text?.indexOf("url,username,password") >= 0); - - if (textContent) { - break; - } - } - - if (textContent) { - this.exportData = textContent; - this.postPortMessage({ command: "displayLpImportNotification" }); - this.mutationObserver.disconnect(); - } - }; - - /** - * If the export data is present, sends a message to the background with - * the export data to start the import process. - */ - private startLpImport() { - if (!this.exportData) { - return; - } - - this.postPortMessage({ command: "startLpImport", data: this.exportData }); - this.messagePort?.disconnect(); - } - - /** - * Posts a message to the background script. - * - * @param message - The message to post. - */ - private postPortMessage(message: LpFilelessImporterMessage) { - this.messagePort?.postMessage(message); - } - - /** - * Posts a message to the global context of the page. - * - * @param message - The message to post. - */ - private postWindowMessage(message: LpFilelessImporterMessage) { - globalThis.postMessage(message, "https://lastpass.com"); - } - - /** - * Sets up the message port that is used to facilitate communication between the - * background script and the content script. - */ - private setupMessagePort() { - this.messagePort = chrome.runtime.connect({ name: FilelessImportPort.LpImporter }); - this.messagePort.onMessage.addListener(this.handlePortMessage); - } - - /** - * Handles messages that are sent from the background script. - * - * @param message - The message that was sent. - * @param port - The port that the message was sent from. - */ - private handlePortMessage = (message: LpFilelessImporterMessage, port: chrome.runtime.Port) => { - const handler = this.portMessageHandlers[message.command]; - if (!handler) { - return; - } - - handler({ message, port }); - }; -} - -(function () { - if (!(globalThis as any).lpFilelessImporter) { - (globalThis as any).lpFilelessImporter = new LpFilelessImporter(); - (globalThis as any).lpFilelessImporter.init(); - } -})(); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts deleted file mode 100644 index 8479235cc17d..000000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -describe("LP Suppress Import Download for Manifest v2", () => { - it("appends the `lp-suppress-import-download.js` script to the document element", () => { - let createdScriptElement: HTMLScriptElement; - jest.spyOn(window.document, "createElement"); - jest.spyOn(window.document.documentElement, "appendChild").mockImplementation((node) => { - createdScriptElement = node as HTMLScriptElement; - return node; - }); - - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-require-imports - require("./lp-suppress-import-download-script-append.mv2"); - - expect(window.document.createElement).toHaveBeenCalledWith("script"); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("content/lp-suppress-import-download.js"); - expect(window.document.documentElement.appendChild).toHaveBeenCalledWith( - expect.any(HTMLScriptElement), - ); - expect(createdScriptElement.src).toBe( - "chrome-extension://id/content/lp-suppress-import-download.js", - ); - }); -}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts deleted file mode 100644 index cd641590ad1b..000000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This script handles injection of the LP suppress import download script into the document. - * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. - */ -(function (globalContext) { - const script = globalContext.document.createElement("script"); - script.src = chrome.runtime.getURL("content/lp-suppress-import-download.js"); - globalContext.document.documentElement.appendChild(script); -})(window); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts deleted file mode 100644 index ff0ed3815997..000000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { flushPromises, postWindowMessage } from "../../autofill/spec/testing-utils"; - -describe("LP Suppress Import Download", () => { - const downloadAttribute = "file.csv"; - const hrefAttribute = "https://example.com/file.csv"; - const overridenHrefAttribute = "javascript:void(0)"; - let anchor: HTMLAnchorElement; - - beforeEach(() => { - jest.spyOn(Element.prototype, "appendChild"); - jest.spyOn(window, "addEventListener"); - - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-require-imports - require("./lp-suppress-import-download"); - - anchor = document.createElement("a"); - anchor.download = downloadAttribute; - anchor.href = hrefAttribute; - anchor.click = jest.fn(); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - it("disables the automatic download anchor", () => { - document.body.appendChild(anchor); - - expect(anchor.href).toBe(overridenHrefAttribute); - expect(anchor.download).toBe(""); - }); - - it("triggers the CSVDownload when receiving a `triggerCsvDownload` window message", async () => { - window.document.createElement = jest.fn(() => anchor); - jest.spyOn(window, "removeEventListener"); - - document.body.appendChild(anchor); - - // Precondition - Ensure the anchor in the document has overridden href and download attributes - expect(anchor.href).toBe(overridenHrefAttribute); - expect(anchor.download).toBe(""); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - expect(anchor.click).toHaveBeenCalled(); - expect(anchor.href).toEqual(hrefAttribute); - expect(anchor.download).toEqual(downloadAttribute); - expect(window.removeEventListener).toHaveBeenCalledWith("message", expect.any(Function)); - }); - - it("skips subsequent calls to trigger a CSVDownload", async () => { - window.document.createElement = jest.fn(() => anchor); - - document.body.appendChild(anchor); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - expect(anchor.click).toHaveBeenCalledTimes(1); - }); - - it("skips triggering the CSV download for window messages that do not have the correct command", () => { - document.body.appendChild(anchor); - - postWindowMessage({ command: "notTriggerCsvDownload" }); - - expect(anchor.click).not.toHaveBeenCalled(); - }); - - it("skips triggering the CSV download for window messages that do not have a data value", () => { - document.body.appendChild(anchor); - - postWindowMessage(null); - - expect(anchor.click).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.ts b/apps/browser/src/tools/content/lp-suppress-import-download.ts deleted file mode 100644 index 1d5d449d1997..000000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download.ts +++ /dev/null @@ -1,52 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -/** - * Handles intercepting the injection of the CSV download link, and ensures the - * download of the script is suppressed until the user opts to download the file. - * The download is triggered by a window message sent from the LpFilelessImporter - * content script. - */ -(function (globalContext) { - let csvDownload = ""; - let csvHref = ""; - let isCsvDownloadTriggered = false; - const defaultAppendChild = Element.prototype.appendChild; - Element.prototype.appendChild = function (newChild: Node) { - if (isAnchorElement(newChild) && newChild.download) { - csvDownload = newChild.download; - csvHref = newChild.href; - newChild.setAttribute("href", "javascript:void(0)"); - newChild.setAttribute("download", ""); - Element.prototype.appendChild = defaultAppendChild; - } - - return defaultAppendChild.call(this, newChild); - }; - - function isAnchorElement(node: Node): node is HTMLAnchorElement { - return node.nodeName.toLowerCase() === "a"; - } - - const handleWindowMessage = (event: MessageEvent) => { - const command = event.data?.command; - if ( - event.source !== globalContext || - command !== "triggerCsvDownload" || - isCsvDownloadTriggered - ) { - return; - } - - isCsvDownloadTriggered = true; - globalContext.removeEventListener("message", handleWindowMessage); - - const anchor = globalContext.document.createElement("a"); - anchor.setAttribute("href", csvHref); - anchor.setAttribute("download", csvDownload); - globalContext.document.body.appendChild(anchor); - anchor.click(); - globalContext.document.body.removeChild(anchor); - }; - - globalContext.addEventListener("message", handleWindowMessage); -})(window); diff --git a/apps/browser/src/tools/enums/fileless-import.enums.ts b/apps/browser/src/tools/enums/fileless-import.enums.ts deleted file mode 100644 index e20f4f154547..000000000000 --- a/apps/browser/src/tools/enums/fileless-import.enums.ts +++ /dev/null @@ -1,12 +0,0 @@ -const FilelessImportType = { - LP: "LP", -} as const; - -type FilelessImportTypeKeys = (typeof FilelessImportType)[keyof typeof FilelessImportType]; - -const FilelessImportPort = { - NotificationBar: "fileless-importer-notification-bar", - LpImporter: "lp-fileless-importer", -} as const; - -export { FilelessImportType, FilelessImportTypeKeys, FilelessImportPort }; diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts index 8b880e886713..a3d1c553977f 100644 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts @@ -6,15 +6,16 @@ import { Observable, firstValueFrom, of, switchMap } 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { DialogService, ItemModule } from "@bitwarden/components"; +import { FamiliesPolicyService } from "../../../../billing/services/families-policy.service"; 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 { FamiliesPolicyService } from "../../../../services/families-policy.service"; @Component({ templateUrl: "more-from-bitwarden-page-v2.component.html", @@ -43,6 +44,9 @@ export class MoreFromBitwardenPageV2Component { private familiesPolicyService: FamiliesPolicyService, private accountService: AccountService, ) { + this.familySponsorshipAvailable$ = getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => this.organizationService.familySponsorshipAvailable$(userId)), + ); this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => account @@ -50,7 +54,6 @@ export class MoreFromBitwardenPageV2Component { : of(false), ), ); - this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$; this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); } diff --git a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts index 86131176a6eb..851509ab17fd 100644 --- a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts @@ -34,6 +34,6 @@ export class ExportBrowserV2Component { constructor(private router: Router) {} protected async onSuccessfulExport(organizationId: string): Promise { - await this.router.navigate(["/vault-settings"]); + await this.router.navigate(["/tabs/settings"]); } } diff --git a/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts index 66cb5c62f483..1c5558bd90e4 100644 --- a/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts @@ -4,7 +4,7 @@ import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components"; -import { ImportComponent } from "@bitwarden/importer/ui"; +import { ImportComponent } from "@bitwarden/importer-ui"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 7ff958e26ac8..26aeea4f20a8 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -8,38 +8,44 @@ - {{ "accountSecurity" | i18n }} + + + {{ "accountSecurity" | i18n }} - {{ "autofill" | i18n }} + + + {{ "autofill" | i18n }} - {{ "notifications" | i18n }} + + + {{ "notifications" | i18n }} - {{ "vault" | i18n }} + + + {{ "vault" | i18n }} - {{ "appearance" | i18n }} + + + {{ "appearance" | i18n }} - {{ "about" | i18n }} + + + {{ "about" | i18n }} 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 a46f5a6955be..152c500d6caa 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 @@ -26,5 +26,15 @@ + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts index ebfb1ff765fa..3252f030fc33 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -1,17 +1,19 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, Observable } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { CipherFormConfig, @@ -40,7 +42,7 @@ describe("AddEditV2Component", () => { const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; const buildConfig = jest.fn((mode: CipherFormMode) => - Promise.resolve({ mode, ...buildConfigResponse }), + Promise.resolve({ ...buildConfigResponse, mode }), ); const queryParams$ = new BehaviorSubject({}); const disable = jest.fn(); @@ -55,9 +57,10 @@ describe("AddEditV2Component", () => { back.mockClear(); collect.mockClear(); - addEditCipherInfo$ = new BehaviorSubject(null); + addEditCipherInfo$ = new BehaviorSubject(null); cipherServiceMock = mock(); - cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable(); + cipherServiceMock.addEditCipherInfo$ = + addEditCipherInfo$.asObservable() as Observable; await TestBed.configureTestingModule({ imports: [AddEditV2Component], @@ -71,6 +74,13 @@ describe("AddEditV2Component", () => { { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: CipherService, useValue: cipherServiceMock }, { provide: EventCollectionService, useValue: { collect } }, + { provide: LogService, useValue: mock() }, + { + provide: CipherAuthorizationService, + useValue: { + canDeleteCipher$: jest.fn().mockReturnValue(true), + }, + }, ], }) .overrideProvider(CipherFormConfigService, { @@ -92,7 +102,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("add"); + expect(buildConfig.mock.lastCall![0]).toBe("add"); expect(component.config.mode).toBe("add"); })); @@ -101,7 +111,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("clone"); + expect(buildConfig.mock.lastCall![0]).toBe("clone"); expect(component.config.mode).toBe("clone"); })); @@ -111,7 +121,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("edit"); })); @@ -121,7 +131,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("partial-edit"); })); }); @@ -218,7 +228,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.username).toBe("identity-username"); + expect(component.config.initialValues!.username).toBe("identity-username"); })); it("overrides query params with `addEditCipherInfo` values", fakeAsync(() => { @@ -231,7 +241,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.name).toBe("AddEditCipherName"); + expect(component.config.initialValues!.name).toBe("AddEditCipherName"); })); it("clears `addEditCipherInfo` after initialization", fakeAsync(() => { @@ -326,4 +336,30 @@ describe("AddEditV2Component", () => { expect(back).toHaveBeenCalled(); }); }); + + describe("delete", () => { + it("dialogService openSimpleDialog called when deleteBtn is hit", async () => { + const dialogSpy = jest + .spyOn(component["dialogService"], "openSimpleDialog") + .mockResolvedValue(true); + + await component.delete(); + expect(dialogSpy).toHaveBeenCalled(); + }); + + it("should call deleteCipher when user confirms deletion", async () => { + const deleteCipherSpy = jest.spyOn(component as any, "deleteCipher"); + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + + await component.delete(); + expect(deleteCipherSpy).toHaveBeenCalled(); + }); + + it("navigates to vault tab after deletion", async () => { + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + await component.delete(); + + expect(navigate).toHaveBeenCalledWith(["/tabs/vault"]); + }); + }); }); 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 2d8c4857c1cb..b46b1d615097 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 @@ -5,18 +5,27 @@ import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom, map, switchMap } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { 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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; -import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + ButtonModule, + SearchModule, + IconButtonModule, + DialogService, + ToastService, +} from "@bitwarden/components"; import { CipherFormConfig, CipherFormConfigService, @@ -131,11 +140,13 @@ export type AddEditQueryParams = Partial>; CipherFormModule, AsyncActionsModule, PopOutComponent, + IconButtonModule, ], }) export class AddEditV2Component implements OnInit { headerText: string; config: CipherFormConfig; + canDeleteCipher$: Observable; get loading() { return this.config == null; @@ -165,6 +176,10 @@ export class AddEditV2Component implements OnInit { private router: Router, private cipherService: CipherService, private eventCollectionService: EventCollectionService, + private logService: LogService, + private toastService: ToastService, + private dialogService: DialogService, + protected cipherAuthorizationService: CipherAuthorizationService, ) { this.subscribeToParams(); } @@ -281,6 +296,10 @@ export class AddEditV2Component implements OnInit { } if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) { + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( + config.originalCipher, + ); + await this.eventCollectionService.collect( EventType.Cipher_ClientViewed, config.originalCipher.id, @@ -337,6 +356,43 @@ export class AddEditV2Component implements OnInit { return this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); } } + + delete = async () => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.deleteCipher(); + } catch (e) { + this.logService.error(e); + return false; + } + + await this.router.navigate(["/tabs/vault"]); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedItem"), + }); + + return true; + }; + + protected deleteCipher() { + return this.config.originalCipher.deletedDate + ? this.cipherService.deleteWithServer(this.config.originalCipher.id) + : this.cipherService.softDeleteWithServer(this.config.originalCipher.id); + } } /** 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 4f6c4aa07cf8..66d9096cd5c9 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 @@ -50,7 +50,7 @@ describe("OpenAttachmentsComponent", () => { } as Organization; const getCipher = jest.fn().mockResolvedValue(cipherDomain); - const getOrganization = jest.fn().mockResolvedValue(org); + const organizations$ = jest.fn().mockReturnValue(of([org])); const showFilePopoutMessage = jest.fn().mockReturnValue(false); const mockUserId = Utils.newGuid() as UserId; @@ -67,7 +67,7 @@ describe("OpenAttachmentsComponent", () => { openCurrentPagePopout.mockClear(); getCipher.mockClear(); showToast.mockClear(); - getOrganization.mockClear(); + organizations$.mockClear(); showFilePopoutMessage.mockClear(); hasPremiumFromAnySource$.next(true); @@ -89,7 +89,7 @@ describe("OpenAttachmentsComponent", () => { }, { provide: OrganizationService, - useValue: { get: getOrganization }, + useValue: { organizations$ }, }, { provide: FilePopoutUtilsService, @@ -148,11 +148,11 @@ describe("OpenAttachmentsComponent", () => { describe("Free Orgs", () => { beforeEach(() => { - component.cipherIsAPartOfFreeOrg = undefined; + component.cipherIsAPartOfFreeOrg = false; }); it("sets `cipherIsAPartOfFreeOrg` to false when the cipher is not a part of an organization", async () => { - cipherView.organizationId = null; + cipherView.organizationId = ""; await component.ngOnInit(); @@ -162,6 +162,7 @@ describe("OpenAttachmentsComponent", () => { it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => { cipherView.organizationId = "888-333-333"; org.productTierType = ProductTierType.Free; + org.id = cipherView.organizationId; await component.ngOnInit(); @@ -171,6 +172,7 @@ describe("OpenAttachmentsComponent", () => { it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => { cipherView.organizationId = "888-333-333"; org.productTierType = ProductTierType.Families; + org.id = cipherView.organizationId; await component.ngOnInit(); 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 5e27ccd5c41f..aca494716b1a 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 @@ -7,8 +7,12 @@ import { Router } from "@angular/router"; import { firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -86,7 +90,12 @@ export class OpenAttachmentsComponent implements OnInit { return; } - const org = await this.organizationService.get(cipher.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(cipher.organizationId)), + ); this.cipherIsAPartOfFreeOrg = org.productTierType === ProductTierType.Free; } diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index eae8e2cc9803..071873b40c99 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -7,4 +7,5 @@ [description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null" showAutofillButton [primaryActionAutofill]="clickItemsToAutofillVaultView" + [groupByType]="groupByType()" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts index d2dd21be6d87..03d84120785a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts @@ -1,5 +1,6 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; import { combineLatest, firstValueFrom, map, Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -48,6 +49,10 @@ export class AutofillVaultListItemsComponent implements OnInit { clickItemsToAutofillVaultView = false; + protected groupByType = toSignal( + this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)), + ); + /** * Observable that determines whether the empty autofill tip should be shown. * The tip is shown when there are no login ciphers to autofill, no filter is applied, and autofill is allowed in diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index 4c7067df53a0..6e6e30b359b5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -27,7 +27,7 @@ - + {{ "clone" | 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 5d3dee9018ec..94b4c2b855bc 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 @@ -9,6 +9,7 @@ import { filter } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; @@ -88,13 +89,17 @@ export class ItemMoreOptionsComponent implements OnInit { ) {} async ngOnInit(): Promise { - this.hasOrganizations = await this.organizationService.hasOrganizations(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.hasOrganizations = await firstValueFrom(this.organizationService.hasOrganizations(userId)); } get canEdit() { return this.cipher.edit; } + get canViewPassword() { + return this.cipher.viewPassword; + } /** * Determines if the cipher can be autofilled. */ 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 d57b1d2fe366..db3fff04bbb0 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 @@ -11,11 +11,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components"; +import { AddEditFolderDialogComponent } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; 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; @@ -72,6 +72,6 @@ export class NewItemDropdownV2Component implements OnInit { } openFolderDialog() { - this.dialogService.open(AddEditFolderDialogComponent); + AddEditFolderDialogComponent.open(this.dialogService); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html index 5f958433c6db..91feaa433a91 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html @@ -20,7 +20,7 @@

{{ numberOfAppliedFilters$ | async }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts index 3b9dc9a16470..bcea2e76190c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts @@ -6,11 +6,12 @@ import { combineLatest, map, take } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DisclosureTriggerForDirective, IconButtonModule } from "@bitwarden/components"; +import { + DisclosureComponent, + DisclosureTriggerForDirective, + IconButtonModule, +} from "@bitwarden/components"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { DisclosureComponent } from "../../../../../../../../libs/components/src/disclosure/disclosure.component"; import { runInsideAngular } from "../../../../../platform/browser/run-inside-angular.operator"; import { VaultPopupListFiltersService } from "../../../../../vault/popup/services/vault-popup-list-filters.service"; import { VaultListFiltersComponent } from "../vault-list-filters/vault-list-filters.component"; 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 56f35c41f6d2..c61562f9f901 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 @@ -2,8 +2,9 @@
- + - + - + { + return { + organizations, + collections, + folders, + }; + }), + ); + constructor(private vaultPopupListFiltersService: VaultPopupListFiltersService) {} } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index c55e8d9fb265..2272d3fbd6c1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -1,9 +1,13 @@ - + - - - - - - - - - - - - + + +

+ {{ group.subHeaderKey | i18n }} +

+
+ + + + + + + + + + + + + + + + + +
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 725aaac46667..f95790cda5fd 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 @@ -9,11 +9,14 @@ import { EventEmitter, inject, Input, - OnInit, Output, Signal, signal, ViewChild, + computed, + OnInit, + ChangeDetectionStrategy, + input, } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom, Observable, map } from "rxjs"; @@ -23,6 +26,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherId } 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 { BadgeModule, @@ -73,6 +77,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options selector: "app-vault-list-items-container", templateUrl: "vault-list-items-container.component.html", standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, }) export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { private compactModeService = inject(CompactModeService); @@ -110,11 +115,51 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { */ private viewCipherTimeout: number | null; + ciphers = input([]); + /** - * The list of ciphers to display. + * If true, we will group ciphers by type (Login, Card, Identity) + * within subheadings in a single container, converted to a WritableSignal. */ - @Input() - ciphers: PopupCipherView[] = []; + groupByType = input(false); + + /** + * Computed signal for a grouped list of ciphers with an optional header + */ + cipherGroups$ = computed< + { + subHeaderKey?: string | null; + ciphers: PopupCipherView[]; + }[] + >(() => { + const groups: { [key: string]: CipherView[] } = {}; + + this.ciphers().forEach((cipher) => { + let groupKey; + + if (this.groupByType()) { + switch (cipher.type) { + case CipherType.Card: + groupKey = "cards"; + break; + case CipherType.Identity: + groupKey = "identities"; + break; + } + } + + if (!groups[groupKey]) { + groups[groupKey] = []; + } + + groups[groupKey].push(cipher); + }); + + return Object.keys(groups).map((key) => ({ + subHeaderKey: this.groupByType ? key : "", + ciphers: groups[key], + })); + }); /** * Title for the vault list item section. diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts index 67e069d388a0..e20bb1f1bcd5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts @@ -11,10 +11,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { PasswordHistoryViewComponent } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.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"; diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 43471e56e7bd..6e0170427110 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -69,13 +69,13 @@ { + this.vaultScrollPositionService.start(this.virtualScrollElement!); + }); + } + } + async ngOnInit() { this.cipherService.failedToDecryptCiphers$ .pipe( @@ -134,5 +150,7 @@ export class VaultV2Component implements OnInit, OnDestroy { }); } - ngOnDestroy(): void {} + ngOnDestroy(): void { + this.vaultScrollPositionService.stop(); + } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 526ab2e25790..39feb86f4fd0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -21,12 +21,15 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { 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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { DialogService, ToastService } from "@bitwarden/components"; import { CopyCipherFieldService } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; +import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service"; import { ViewV2Component } from "./view-v2.component"; @@ -44,6 +47,10 @@ describe("ViewV2Component", () => { const collect = jest.fn().mockResolvedValue(null); const doAutofill = jest.fn().mockResolvedValue(true); const copy = jest.fn().mockResolvedValue(true); + const back = jest.fn().mockResolvedValue(null); + const openSimpleDialog = jest.fn().mockResolvedValue(true); + const stop = jest.fn(); + const showToast = jest.fn(); const mockCipher = { id: "122-333-444", @@ -54,7 +61,7 @@ describe("ViewV2Component", () => { password: "test-password", totp: "123", }, - }; + } as unknown as CipherView; const mockVaultPopupAutofillService = { doAutofill, @@ -68,13 +75,21 @@ describe("ViewV2Component", () => { const mockCipherService = { get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }), getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}), + deleteWithServer: jest.fn().mockResolvedValue(undefined), + softDeleteWithServer: jest.fn().mockResolvedValue(undefined), }; beforeEach(async () => { + mockCipherService.deleteWithServer.mockClear(); + mockCipherService.softDeleteWithServer.mockClear(); mockNavigate.mockClear(); collect.mockClear(); doAutofill.mockClear(); copy.mockClear(); + stop.mockClear(); + openSimpleDialog.mockClear(); + back.mockClear(); + showToast.mockClear(); await TestBed.configureTestingModule({ imports: [ViewV2Component], @@ -84,9 +99,12 @@ describe("ViewV2Component", () => { { provide: LogService, useValue: mock() }, { provide: PlatformUtilsService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, - { provide: PopupRouterCacheService, useValue: mock() }, + { provide: PopupRouterCacheService, useValue: mock({ back }) }, { provide: ActivatedRoute, useValue: { queryParams: params$ } }, { provide: EventCollectionService, useValue: { collect } }, + { provide: VaultPopupScrollPositionService, useValue: { stop } }, + { provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService }, + { provide: ToastService, useValue: { showToast } }, { provide: I18nService, useValue: { @@ -98,7 +116,6 @@ describe("ViewV2Component", () => { }, }, }, - { provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService }, { provide: AccountService, useValue: accountService, @@ -114,7 +131,13 @@ describe("ViewV2Component", () => { useValue: mockCopyCipherFieldService, }, ], - }).compileComponents(); + }) + .overrideProvider(DialogService, { + useValue: { + openSimpleDialog, + }, + }) + .compileComponents(); fixture = TestBed.createComponent(ViewV2Component); component = fixture.componentInstance; @@ -223,4 +246,130 @@ describe("ViewV2Component", () => { expect(closeSpy).toHaveBeenCalledTimes(1); })); }); + + describe("delete", () => { + beforeEach(() => { + component.cipher = mockCipher; + }); + + it("opens confirmation modal", async () => { + await component.delete(); + + expect(openSimpleDialog).toHaveBeenCalledTimes(1); + }); + + it("navigates back", async () => { + await component.delete(); + + expect(back).toHaveBeenCalledTimes(1); + }); + + it("stops scroll position service", async () => { + await component.delete(); + + expect(stop).toHaveBeenCalledTimes(1); + expect(stop).toHaveBeenCalledWith(true); + }); + + describe("deny confirmation", () => { + beforeEach(() => { + openSimpleDialog.mockResolvedValue(false); + }); + + it("does not delete the cipher", async () => { + await component.delete(); + + expect(mockCipherService.deleteWithServer).not.toHaveBeenCalled(); + expect(mockCipherService.softDeleteWithServer).not.toHaveBeenCalled(); + }); + + it("does not interact with side effects", () => { + expect(back).not.toHaveBeenCalled(); + expect(stop).not.toHaveBeenCalled(); + expect(showToast).not.toHaveBeenCalled(); + }); + }); + + describe("accept confirmation", () => { + beforeEach(() => { + openSimpleDialog.mockResolvedValue(true); + }); + + describe("soft delete", () => { + beforeEach(() => { + (mockCipher as any).isDeleted = null; + }); + + it("opens confirmation dialog", async () => { + await component.delete(); + + expect(openSimpleDialog).toHaveBeenCalledTimes(1); + expect(openSimpleDialog).toHaveBeenCalledWith({ + content: { + key: "deleteItemConfirmation", + }, + title: { + key: "deleteItem", + }, + type: "warning", + }); + }); + + it("calls soft delete", async () => { + await component.delete(); + + expect(mockCipherService.softDeleteWithServer).toHaveBeenCalled(); + expect(mockCipherService.deleteWithServer).not.toHaveBeenCalled(); + }); + + it("shows toast", async () => { + await component.delete(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "deletedItem", + }); + }); + }); + + describe("hard delete", () => { + beforeEach(() => { + (mockCipher as any).isDeleted = true; + }); + + it("opens confirmation dialog", async () => { + await component.delete(); + + expect(openSimpleDialog).toHaveBeenCalledTimes(1); + expect(openSimpleDialog).toHaveBeenCalledWith({ + content: { + key: "permanentlyDeleteItemConfirmation", + }, + title: { + key: "deleteItem", + }, + type: "warning", + }); + }); + + it("calls soft delete", async () => { + await component.delete(); + + expect(mockCipherService.deleteWithServer).toHaveBeenCalled(); + expect(mockCipherService.softDeleteWithServer).not.toHaveBeenCalled(); + }); + + it("shows toast", async () => { + await component.delete(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "permanentlyDeletedItem", + }); + }); + }); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 6532fb004cbf..65fb024ee998 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -23,6 +23,7 @@ import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -35,20 +36,15 @@ import { SearchModule, ToastService, } from "@bitwarden/components"; -import { CopyCipherFieldService } from "@bitwarden/vault"; +import { CipherViewComponent, CopyCipherFieldService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service"; import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service"; +import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window"; import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; @@ -113,6 +109,7 @@ export class ViewV2Component { private popupRouterCacheService: PopupRouterCacheService, protected cipherAuthorizationService: CipherAuthorizationService, private copyCipherFieldService: CopyCipherFieldService, + private popupScrollPositionService: VaultPopupScrollPositionService, ) { this.subscribeToParams(); } @@ -202,6 +199,7 @@ export class ViewV2Component { return false; } + this.popupScrollPositionService.stop(true); await this.popupRouterCacheService.back(); this.toastService.showToast({ @@ -235,7 +233,11 @@ export class ViewV2Component { } protected showFooter(): boolean { - return this.cipher && (!this.cipher.isDeleted || (this.cipher.isDeleted && this.cipher.edit)); + return ( + this.cipher && + (!this.cipher.isDeleted || + (this.cipher.isDeleted && this.cipher.edit && this.cipher.viewPassword)) + ); } /** 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 2dad1e3034c7..75352c1331aa 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 @@ -21,14 +21,12 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutoFillOptions, AutofillService, PageDetail, } from "../../../autofill/services/abstractions/autofill.service"; +import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; 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 ff282d7a6d06..82188ef823bf 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 @@ -27,13 +27,11 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutofillService, PageDetail, } from "../../../autofill/services/abstractions/autofill.service"; +import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { closeViewVaultItemPopout, VaultPopoutType } from "../utils/vault-popout-window"; 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 528aae111cc9..ec20458ca602 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 @@ -6,18 +6,18 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { ObservableTracker } from "@bitwarden/common/spec"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { ObservableTracker, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; +import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; @@ -43,6 +43,8 @@ describe("VaultPopupItemsService", () => { const vaultAutofillServiceMock = mock(); const syncServiceMock = mock(); const inlineMenuFieldQualificationServiceMock = mock(); + const userId = Utils.newGuid() as UserId; + const accountServiceMock = mockAccountServiceWith(userId); beforeEach(() => { allCiphers = cipherFactory(10); @@ -99,7 +101,7 @@ describe("VaultPopupItemsService", () => { { id: "col2", name: "Collection 2" } as CollectionView, ]; - organizationServiceMock.organizations$ = new BehaviorSubject([mockOrg]); + organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg])); collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections); activeUserLastSync$ = new BehaviorSubject(new Date()); @@ -111,6 +113,7 @@ describe("VaultPopupItemsService", () => { { provide: VaultSettingsService, useValue: vaultSettingsServiceMock }, { provide: SearchService, useValue: searchService }, { provide: OrganizationService, useValue: organizationServiceMock }, + { provide: AccountService, useValue: accountServiceMock }, { provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock }, { provide: CollectionService, useValue: collectionService }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, 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 fb230df7953f..8e0711abb1ec 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 @@ -24,6 +24,7 @@ import { import { CollectionService } from "@bitwarden/admin-console/common"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; @@ -56,6 +57,9 @@ export class VaultPopupItemsService { latestSearchText$: Observable = this._searchText$.asObservable(); + private organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + ); /** * Observable that contains the list of other cipher types that should be shown * in the autofill section of the Vault tab. Depends on vault settings. @@ -67,9 +71,9 @@ export class VaultPopupItemsService { this.vaultPopupAutofillService.nonLoginCipherTypesOnPage$, ]).pipe( map(([showCardsSettingEnabled, showIdentitiesSettingEnabled, nonLoginCipherTypesOnPage]) => { - const showCards = showCardsSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Card]; + const showCards = showCardsSettingEnabled || nonLoginCipherTypesOnPage[CipherType.Card]; const showIdentities = - showIdentitiesSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Identity]; + showIdentitiesSettingEnabled || nonLoginCipherTypesOnPage[CipherType.Identity]; return [ ...(showCards ? [CipherType.Card] : []), @@ -97,10 +101,7 @@ export class VaultPopupItemsService { private _activeCipherList$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([ - this.organizationService.organizations$, - this.collectionService.decryptedCollections$, - ]).pipe( + combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); @@ -213,6 +214,7 @@ export class VaultPopupItemsService { map(([hasSearchText, filters]) => { return hasSearchText || Object.values(filters).some((filter) => filter !== null); }), + shareReplay({ bufferSize: 1, refCount: true }), ); /** @@ -232,7 +234,7 @@ export class VaultPopupItemsService { /** Observable that indicates when the user should see the deactivated org state */ showDeactivatedOrg$: Observable = combineLatest([ this.vaultPopupListFiltersService.filters$.pipe(distinctUntilKeyChanged("organization")), - this.organizationService.organizations$, + this.organizations$, ]).pipe( map(([filters, orgs]) => { if (!filters.organization || filters.organization.id === MY_VAULT_ID) { @@ -249,10 +251,7 @@ export class VaultPopupItemsService { */ deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([ - this.organizationService.organizations$, - this.collectionService.decryptedCollections$, - ]).pipe( + combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); @@ -281,6 +280,7 @@ export class VaultPopupItemsService { private collectionService: CollectionService, private vaultPopupAutofillService: VaultPopupAutofillService, private syncService: SyncService, + private accountService: AccountService, ) {} applyFilter(newSearchText: string) { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index e1236be08f94..7f570e8f5c91 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -23,7 +23,9 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi describe("VaultPopupListFiltersService", () => { let service: VaultPopupListFiltersService; - const memberOrganizations$ = new BehaviorSubject([]); + const _memberOrganizations$ = new BehaviorSubject([]); + const memberOrganizations$ = (userId: UserId) => _memberOrganizations$; + const organizations$ = new BehaviorSubject([]); const folderViews$ = new BehaviorSubject([]); const cipherViews$ = new BehaviorSubject({}); const decryptedCollections$ = new BehaviorSubject([]); @@ -44,6 +46,7 @@ describe("VaultPopupListFiltersService", () => { const organizationService = { memberOrganizations$, + organizations$, } as unknown as OrganizationService; const i18nService = { @@ -58,7 +61,7 @@ describe("VaultPopupListFiltersService", () => { const update = jest.fn().mockResolvedValue(undefined); beforeEach(() => { - memberOrganizations$.next([]); + _memberOrganizations$.next([]); decryptedCollections$.next([]); policyAppliesToActiveUser$.next(false); policyService.policyAppliesToActiveUser$.mockClear(); @@ -135,7 +138,7 @@ describe("VaultPopupListFiltersService", () => { describe("organizations$", () => { it('does not add "myVault" to the list of organizations when there are no organizations', (done) => { - memberOrganizations$.next([]); + _memberOrganizations$.next([]); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([]); @@ -145,7 +148,7 @@ describe("VaultPopupListFiltersService", () => { it('adds "myVault" to the list of organizations when there are other organizations', (done) => { const orgs = [{ name: "bobby's org", id: "1234-3323-23223" }] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual(["myVault", "bobby's org"]); @@ -158,7 +161,7 @@ describe("VaultPopupListFiltersService", () => { { name: "bobby's org", id: "1234-3323-23223" }, { name: "alice's org", id: "2223-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -179,7 +182,7 @@ describe("VaultPopupListFiltersService", () => { it("returns an empty array when the policy applies and there is a single organization", (done) => { policyAppliesToActiveUser$.next(true); - memberOrganizations$.next([ + _memberOrganizations$.next([ { name: "bobby's org", id: "1234-3323-23223" }, ] as Organization[]); @@ -196,7 +199,7 @@ describe("VaultPopupListFiltersService", () => { { name: "alice's org", id: "2223-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -216,7 +219,7 @@ describe("VaultPopupListFiltersService", () => { { name: "catherine's org", id: "77733-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -240,7 +243,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual(["bwi-user", "bwi-family"]); @@ -258,7 +261,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual(["bwi-user", "bwi-family"]); @@ -276,7 +279,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual([ diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 8455fd587d01..579319c92ab3 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -208,7 +208,9 @@ export class VaultPopupListFiltersService { * Organization array structured to be directly passed to `ChipSelectComponent` */ organizations$: Observable[]> = combineLatest([ - this.organizationService.memberOrganizations$, + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.memberOrganizations$(account?.id)), + ), this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), ]).pipe( map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ @@ -368,6 +370,9 @@ export class VaultPopupListFiltersService { ), ); + /** Organizations, collection, folders filters. */ + allFilters$ = combineLatest([this.organizations$, this.collections$, this.folders$]); + /** Updates the stored state for filter visibility. */ async updateFilterVisibility(isVisible: boolean): Promise { await this.filterVisibilityState.update(() => isVisible); diff --git a/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.spec.ts new file mode 100644 index 000000000000..562375f8f857 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.spec.ts @@ -0,0 +1,137 @@ +import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling"; +import { fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { NavigationEnd, Router } from "@angular/router"; +import { Subject, Subscription } from "rxjs"; + +import { VaultPopupScrollPositionService } from "./vault-popup-scroll-position.service"; + +describe("VaultPopupScrollPositionService", () => { + let service: VaultPopupScrollPositionService; + const events$ = new Subject(); + const unsubscribe = jest.fn(); + + beforeEach(async () => { + unsubscribe.mockClear(); + + await TestBed.configureTestingModule({ + providers: [ + VaultPopupScrollPositionService, + { provide: Router, useValue: { events: events$ } }, + ], + }); + + service = TestBed.inject(VaultPopupScrollPositionService); + + // set up dummy values + service["scrollPosition"] = 234; + service["scrollSubscription"] = { unsubscribe } as unknown as Subscription; + }); + + describe("router events", () => { + it("does not reset service when navigating to `/tabs/vault`", fakeAsync(() => { + const event = new NavigationEnd(22, "/tabs/vault", ""); + events$.next(event); + + tick(); + + expect(service["scrollPosition"]).toBe(234); + expect(service["scrollSubscription"]).not.toBeNull(); + })); + + it("resets values when navigating to other tab pages", fakeAsync(() => { + const event = new NavigationEnd(23, "/tabs/generator", ""); + events$.next(event); + + tick(); + + expect(service["scrollPosition"]).toBeNull(); + expect(unsubscribe).toHaveBeenCalled(); + expect(service["scrollSubscription"]).toBeNull(); + })); + }); + + describe("stop", () => { + it("removes scroll listener", () => { + service.stop(); + + expect(unsubscribe).toHaveBeenCalledTimes(1); + expect(service["scrollSubscription"]).toBeNull(); + }); + + it("resets stored values", () => { + service.stop(true); + + expect(service["scrollPosition"]).toBeNull(); + }); + }); + + describe("start", () => { + const elementScrolled$ = new Subject(); + const focus = jest.fn(); + const nativeElement = { + scrollTop: 0, + querySelector: jest.fn(() => ({ focus })), + addEventListener: jest.fn(), + style: { + visibility: "", + }, + }; + const virtualElement = { + elementScrolled: () => elementScrolled$, + getElementRef: () => ({ nativeElement }), + scrollTo: jest.fn(), + } as unknown as CdkVirtualScrollableElement; + + afterEach(() => { + // remove the actual subscription created by `.subscribe` + service["scrollSubscription"]?.unsubscribe(); + }); + + describe("initial scroll position", () => { + beforeEach(() => { + (virtualElement.scrollTo as jest.Mock).mockClear(); + nativeElement.querySelector.mockClear(); + }); + + it("does not scroll when `scrollPosition` is null", () => { + service["scrollPosition"] = null; + + service.start(virtualElement); + + expect(virtualElement.scrollTo).not.toHaveBeenCalled(); + }); + + it("scrolls the virtual element to `scrollPosition`", fakeAsync(() => { + service["scrollPosition"] = 500; + nativeElement.scrollTop = 500; + + service.start(virtualElement); + tick(); + + expect(virtualElement.scrollTo).toHaveBeenCalledWith({ behavior: "instant", top: 500 }); + })); + }); + + describe("scroll listener", () => { + it("unsubscribes from any existing subscription", () => { + service.start(virtualElement); + + expect(unsubscribe).toHaveBeenCalled(); + }); + + it("subscribes to `elementScrolled`", fakeAsync(() => { + virtualElement.measureScrollOffset = jest.fn(() => 455); + + service.start(virtualElement); + + elementScrolled$.next(null); // first subscription is skipped by `skip(1)` + elementScrolled$.next(null); + tick(); + + expect(virtualElement.measureScrollOffset).toHaveBeenCalledTimes(1); + expect(virtualElement.measureScrollOffset).toHaveBeenCalledWith("top"); + expect(service["scrollPosition"]).toBe(455); + })); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.ts b/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.ts new file mode 100644 index 000000000000..5bfe0ec93314 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.ts @@ -0,0 +1,81 @@ +import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling"; +import { inject, Injectable } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { NavigationEnd, Router } from "@angular/router"; +import { filter, skip, Subscription } from "rxjs"; + +@Injectable({ + providedIn: "root", +}) +export class VaultPopupScrollPositionService { + private router = inject(Router); + + /** Path of the vault screen */ + private readonly vaultPath = "/tabs/vault"; + + /** Current scroll position relative to the top of the viewport. */ + private scrollPosition: number | null = null; + + /** Subscription associated with the virtual scroll element. */ + private scrollSubscription: Subscription | null = null; + + constructor() { + this.router.events + .pipe( + takeUntilDestroyed(), + filter((event): event is NavigationEnd => event instanceof NavigationEnd), + ) + .subscribe((event) => { + this.resetListenerForNavigation(event); + }); + } + + /** Scrolls the user to the stored scroll position and starts tracking scroll of the page. */ + start(virtualScrollElement: CdkVirtualScrollableElement) { + if (this.hasScrollPosition()) { + // Use `setTimeout` to scroll after rendering is complete + setTimeout(() => { + virtualScrollElement.scrollTo({ top: this.scrollPosition!, behavior: "instant" }); + }); + } + + this.scrollSubscription?.unsubscribe(); + + // Skip the first scroll event to avoid settings the scroll from the above `scrollTo` call + this.scrollSubscription = virtualScrollElement + ?.elementScrolled() + .pipe(skip(1)) + .subscribe(() => { + const offset = virtualScrollElement.measureScrollOffset("top"); + this.scrollPosition = offset; + }); + } + + /** Stops the scroll listener from updating the stored location. */ + stop(reset?: true) { + this.scrollSubscription?.unsubscribe(); + this.scrollSubscription = null; + + if (reset) { + this.scrollPosition = null; + } + } + + /** Returns true when a scroll position has been stored. */ + hasScrollPosition() { + return this.scrollPosition !== null; + } + + /** Conditionally resets the scroll listeners based on the ending path of the navigation */ + private resetListenerForNavigation(event: NavigationEnd): void { + // The vault page is the target of the scroll listener, return early + if (event.url === this.vaultPath) { + return; + } + + // For all other tab pages reset the scroll position + if (event.url.startsWith("/tabs/")) { + this.stop(true); + } + } +} diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index 3aab9f935e48..deddbd444fce 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -14,17 +14,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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 { BadgeModule, CheckboxModule, Option } from "@bitwarden/components"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { CardComponent } from "../../../../../../libs/components/src/card/card.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { SelectModule } from "../../../../../../libs/components/src/select/select.module"; +import { + BadgeModule, + CardComponent, + CheckboxModule, + FormFieldModule, + Option, + SelectModule, +} from "@bitwarden/components"; + import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; 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 index 9c202e26fefc..6689f5a6c6db 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts @@ -14,10 +14,10 @@ import { UserId } from "@bitwarden/common/types/guid"; 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 { AddEditFolderDialogComponent } from "@bitwarden/vault"; 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"; @@ -27,8 +27,8 @@ import { FoldersV2Component } from "./folders-v2.component"; template: ``, }) class MockPopupHeaderComponent { - @Input() pageTitle: string; - @Input() backAction: () => void; + @Input() pageTitle: string = ""; + @Input() backAction: () => void = () => {}; } @Component({ @@ -37,14 +37,15 @@ class MockPopupHeaderComponent { template: ``, }) class MockPopupFooterComponent { - @Input() pageTitle: string; + @Input() pageTitle: string = ""; } describe("FoldersV2Component", () => { let component: FoldersV2Component; let fixture: ComponentFixture; const folderViews$ = new BehaviorSubject([]); - const open = jest.fn(); + const open = jest.spyOn(AddEditFolderDialogComponent, "open"); + const mockDialogService = { open: jest.fn() }; beforeEach(async () => { open.mockClear(); @@ -68,7 +69,7 @@ describe("FoldersV2Component", () => { imports: [MockPopupHeaderComponent, MockPopupFooterComponent], }, }) - .overrideProvider(DialogService, { useValue: { open } }) + .overrideProvider(DialogService, { useValue: mockDialogService }) .compileComponents(); fixture = TestBed.createComponent(FoldersV2Component); @@ -101,9 +102,7 @@ describe("FoldersV2Component", () => { editButton.triggerEventHandler("click"); - expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { - data: { editFolderConfig: { folder } }, - }); + expect(open).toHaveBeenCalledWith(mockDialogService, { editFolderConfig: { folder } }); }); it("opens add dialog for new folder when there are no folders", () => { @@ -114,6 +113,6 @@ describe("FoldersV2Component", () => { addButton.triggerEventHandler("click"); - expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { data: {} }); + expect(open).toHaveBeenCalledWith(mockDialogService, {}); }); }); diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.ts index 8abc3f906c04..f71374e5305f 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.ts @@ -12,25 +12,14 @@ import { ButtonModule, DialogService, IconButtonModule, + ItemModule, + NoItemsModule, } from "@bitwarden/components"; -import { VaultIcons } from "@bitwarden/vault"; +import { AddEditFolderDialogComponent, VaultIcons } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ItemGroupComponent } from "../../../../../../libs/components/src/item/item-group.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ItemModule } from "../../../../../../libs/components/src/item/item.module"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -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, @@ -42,7 +31,6 @@ import { PopupPageComponent, PopupHeaderComponent, ItemModule, - ItemGroupComponent, NoItemsModule, IconButtonModule, ButtonModule, @@ -78,8 +66,6 @@ export class FoldersV2Component { // If a folder is provided, the edit variant should be shown const editFolderConfig = folder ? { folder } : undefined; - this.dialogService.open(AddEditFolderDialogComponent, { - data: { editFolderConfig }, - }); + AddEditFolderDialogComponent.open(this.dialogService, { 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 index dcbda9fd96a3..c77ac4f9755d 100644 --- 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 @@ -31,7 +31,7 @@

> {{ cipher.subTitle }} - + - - {{ "important" | i18n }} - {{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }} - - - - -

- -
- - {{ "reTypeMasterPass" | i18n }} - - - -
- -
- - {{ "masterPassHintLabel" | i18n }} - - {{ "masterPassHintDesc" | i18n }} - -
- -
- -
-
- - {{ "checkForBreaches" | i18n }} -
-
- - - - {{ "acceptPolicies" | i18n }}
- {{ - "termsOfService" | i18n - }}, - {{ - "privacyPolicy" | i18n - }} -
-
- -
- - - - - - -
-

- {{ "alreadyHaveAccount" | i18n }} - {{ "logIn" | i18n }} -

- -
- 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 deleted file mode 100644 index 7d3e6dbd00ee..000000000000 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ /dev/null @@ -1,121 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input, OnInit } from "@angular/core"; -import { UntypedFormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; - -import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/auth/components/register.component"; -import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -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 { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { RegisterRequest } from "@bitwarden/common/models/request/register.request"; -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, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { KeyService } from "@bitwarden/key-management"; - -import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; - -@Component({ - selector: "app-register-form", - templateUrl: "./register-form.component.html", -}) -export class RegisterFormComponent extends BaseRegisterComponent implements OnInit { - @Input() queryParamEmail: string; - @Input() queryParamFromOrgInvite: boolean; - @Input() enforcedPolicyOptions: MasterPasswordPolicyOptions; - @Input() referenceDataValue: ReferenceEventRequest; - - showErrorSummary = false; - characterMinimumMessage: string; - - constructor( - formValidationErrorService: FormValidationErrorsService, - formBuilder: UntypedFormBuilder, - loginStrategyService: LoginStrategyServiceAbstraction, - router: Router, - i18nService: I18nService, - keyService: KeyService, - apiService: ApiService, - stateService: StateService, - platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - private policyService: PolicyService, - environmentService: EnvironmentService, - logService: LogService, - auditService: AuditService, - dialogService: DialogService, - acceptOrgInviteService: AcceptOrganizationInviteService, - toastService: ToastService, - ) { - super( - formValidationErrorService, - formBuilder, - loginStrategyService, - router, - i18nService, - keyService, - apiService, - stateService, - platformUtilsService, - passwordGenerationService, - environmentService, - logService, - auditService, - dialogService, - toastService, - ); - this.modifyRegisterRequest = async (request: RegisterRequest) => { - // 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 acceptOrgInviteService.getOrganizationInvite(); - if (orgInvite != null) { - request.organizationUserId = orgInvite.organizationUserId; - request.token = orgInvite.token; - } - // Invite is accepted after login (on deep link redirect). - }; - } - - async ngOnInit() { - await super.ngOnInit(); - this.referenceData = this.referenceDataValue; - if (this.queryParamEmail) { - this.formGroup.get("email")?.setValue(this.queryParamEmail); - } - - if (this.enforcedPolicyOptions != null && this.enforcedPolicyOptions.minLength > 0) { - this.characterMinimumMessage = ""; - } else { - this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength); - } - } - - async submit() { - if ( - this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - this.passwordStrengthResult.score, - this.formGroup.value.masterPassword, - this.enforcedPolicyOptions, - ) - ) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - }); - return; - } - - await super.submit(false); - } -} diff --git a/apps/web/src/app/auth/register-form/register-form.module.ts b/apps/web/src/app/auth/register-form/register-form.module.ts deleted file mode 100644 index b63cb18506df..000000000000 --- a/apps/web/src/app/auth/register-form/register-form.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; - -import { SharedModule } from "../../shared"; - -import { RegisterFormComponent } from "./register-form.component"; - -@NgModule({ - imports: [SharedModule, PasswordCalloutComponent], - declarations: [RegisterFormComponent], - exports: [RegisterFormComponent], -}) -export class RegisterFormModule {} diff --git a/apps/web/src/app/auth/settings/account/account.component.html b/apps/web/src/app/auth/settings/account/account.component.html index 4055f14219cc..c5edc021614e 100644 --- a/apps/web/src/app/auth/settings/account/account.component.html +++ b/apps/web/src/app/auth/settings/account/account.component.html @@ -9,6 +9,26 @@

{{ "changeEmail" | i18n }}

+ + + + @@ -32,7 +52,6 @@

{{ "changeEmail" | i18n }}

- 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 7e1be937a226..e7adec5f80e9 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -1,11 +1,19 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { combineLatest, from, lastValueFrom, map, Observable } from "rxjs"; +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { + combineLatest, + firstValueFrom, + from, + lastValueFrom, + map, + Observable, + Subject, + takeUntil, +} from "rxjs"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; @@ -14,21 +22,22 @@ import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.compone import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component"; import { DeleteAccountDialogComponent } from "./delete-account-dialog.component"; +import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-devices-dialog.component"; @Component({ selector: "app-account", templateUrl: "account.component.html", }) -export class AccountComponent implements OnInit { - @ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true }) - deauthModalRef: ViewContainerRef; +export class AccountComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); - showChangeEmail$: Observable; - showPurgeVault$: Observable; - showDeleteAccount$: Observable; + showChangeEmail$: Observable = new Observable(); + showPurgeVault$: Observable = new Observable(); + showDeleteAccount$: Observable = new Observable(); + verifyNewDeviceLogin: boolean = true; constructor( - private modalService: ModalService, + private accountService: AccountService, private dialogService: DialogService, private userVerificationService: UserVerificationService, private configService: ConfigService, @@ -36,13 +45,17 @@ export class AccountComponent implements OnInit { ) {} async ngOnInit() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.AccountDeprovisioning, ); - const userIsManagedByOrganization$ = this.organizationService.organizations$.pipe( - map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), - ); + const userIsManagedByOrganization$ = this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), + ); const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword()); @@ -76,11 +89,17 @@ export class AccountComponent implements OnInit { !isAccountDeprovisioningEnabled || !userIsManagedByOrganization, ), ); + this.accountService.accountVerifyNewDeviceLogin$ + .pipe(takeUntil(this.destroy$)) + .subscribe((verifyDevices) => { + this.verifyNewDeviceLogin = verifyDevices; + }); } - async deauthorizeSessions() { - await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef); - } + deauthorizeSessions = async () => { + const dialogRef = DeauthorizeSessionsComponent.open(this.dialogService); + await lastValueFrom(dialogRef.closed); + }; purgeVault = async () => { const dialogRef = PurgeVaultComponent.open(this.dialogService); @@ -91,4 +110,14 @@ export class AccountComponent implements OnInit { const dialogRef = DeleteAccountDialogComponent.open(this.dialogService); await lastValueFrom(dialogRef.closed); }; + + setNewDeviceLoginProtection = async () => { + const dialogRef = SetAccountVerifyDevicesDialogComponent.open(this.dialogService); + await lastValueFrom(dialogRef.closed); + }; + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } 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 34e9f734fc06..7e8b69a0c483 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 @@ -28,16 +28,16 @@ '!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600': 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" + class="tw-relative tw-flex tw-size-24 tw-cursor-pointer tw-place-content-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600" [style.background-color]="customColor$ | async" > {{ "dangerZone" | i18n }}
-

- {{ - (accountDeprovisioningEnabled$ | async) && content.children.length === 1 - ? ("dangerZoneDescSingular" | i18n) - : ("dangerZoneDesc" | i18n) - }} -

-
diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.html b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.html index 3867e9d1ca71..ecadacfeed21 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.html +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.html @@ -1,38 +1,21 @@ - +
+ + +

{{ "deauthorizeSessionsDesc" | i18n }}

+ {{ "deauthorizeSessionsWarning" | i18n }} + +
+ + + + +
+
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 57ca0e0ecfca..a7c466d4ffcf 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 @@ -1,6 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @@ -8,33 +7,33 @@ 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-deauthorize-sessions", templateUrl: "deauthorize-sessions.component.html", }) export class DeauthorizeSessionsComponent { - masterPassword: Verification; - formPromise: Promise; + deauthForm = this.formBuilder.group({ + verification: undefined as Verification | undefined, + }); + invalidSecret: boolean = false; constructor( private apiService: ApiService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, + private formBuilder: FormBuilder, private userVerificationService: UserVerificationService, private messagingService: MessagingService, private logService: LogService, private toastService: ToastService, ) {} - async submit() { + submit = async () => { try { - this.formPromise = this.userVerificationService - .buildRequest(this.masterPassword) - .then((request) => this.apiService.postSecurityStamp(request)); - await this.formPromise; + const verification: Verification = this.deauthForm.value.verification!; + const request = await this.userVerificationService.buildRequest(verification); + await this.apiService.postSecurityStamp(request); this.toastService.showToast({ variant: "success", title: this.i18nService.t("sessionsDeauthorized"), @@ -44,5 +43,9 @@ export class DeauthorizeSessionsComponent { } catch (e) { this.logService.error(e); } + }; + + static open(dialogService: DialogService) { + return dialogService.open(DeauthorizeSessionsComponent); } } 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 aa5cfa3c1dce..64d7dc1b0daa 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 @@ -8,7 +8,6 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a 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, ToastService } from "@bitwarden/components"; @Component({ @@ -22,7 +21,6 @@ export class DeleteAccountDialogComponent { constructor( private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, private accountApiService: AccountApiService, private dialogRef: DialogRef, diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index 4f4920270f02..727313638064 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -9,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ProfileResponse } from "@bitwarden/common/models/response/profile.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -49,16 +50,21 @@ export class ProfileComponent implements OnInit, OnDestroy { this.fingerprintMaterial = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.managingOrganization$ = this.configService .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) .pipe( switchMap((isAccountDeprovisioningEnabled) => isAccountDeprovisioningEnabled - ? this.organizationService.organizations$.pipe( - map((organizations) => - organizations.find((o) => o.userIsManagedByOrganization === true), - ), - ) + ? this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => + organizations.find((o) => o.userIsManagedByOrganization === true), + ), + ) : of(null), ), ); diff --git a/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.html b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.html new file mode 100644 index 000000000000..6cd5bbf92124 --- /dev/null +++ b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.html @@ -0,0 +1,43 @@ +
+ + +

+ {{ "turnOffNewDeviceLoginProtectionModalDesc" | i18n }} +

+

+ {{ "turnOnNewDeviceLoginProtectionModalDesc" | i18n }} +

+ {{ + "turnOffNewDeviceLoginProtectionWarning" | i18n + }} + +
+ + + + + +
+
diff --git a/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts new file mode 100644 index 000000000000..dc7735d75201 --- /dev/null +++ b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts @@ -0,0 +1,122 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { firstValueFrom, Subject, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-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 { SetVerifyDevicesRequest } from "@bitwarden/common/auth/models/request/set-verify-devices.request"; +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 { + AsyncActionsModule, + ButtonModule, + CalloutModule, + DialogModule, + DialogService, + FormFieldModule, + IconButtonModule, + RadioButtonModule, + SelectModule, + ToastService, +} from "@bitwarden/components"; + +@Component({ + templateUrl: "./set-account-verify-devices-dialog.component.html", + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + JslibModule, + FormFieldModule, + AsyncActionsModule, + ButtonModule, + IconButtonModule, + SelectModule, + CalloutModule, + RadioButtonModule, + DialogModule, + UserVerificationFormInputComponent, + ], +}) +export class SetAccountVerifyDevicesDialogComponent implements OnInit, OnDestroy { + // use this subject for all subscriptions to ensure all subscripts are completed + private destroy$ = new Subject(); + // the default for new device verification is true + verifyNewDeviceLogin: boolean = true; + has2faConfigured: boolean = false; + + setVerifyDevicesForm = this.formBuilder.group({ + verification: undefined as Verification | undefined, + }); + invalidSecret: boolean = false; + + constructor( + private i18nService: I18nService, + private formBuilder: FormBuilder, + private accountApiService: AccountApiService, + private accountService: AccountService, + private userVerificationService: UserVerificationService, + private dialogRef: DialogRef, + private toastService: ToastService, + private apiService: ApiService, + ) { + this.accountService.accountVerifyNewDeviceLogin$ + .pipe(takeUntil(this.destroy$)) + .subscribe((verifyDevices: boolean) => { + this.verifyNewDeviceLogin = verifyDevices; + }); + } + + async ngOnInit() { + const twoFactorProviders = await this.apiService.getTwoFactorProviders(); + this.has2faConfigured = twoFactorProviders.data.length > 0; + } + + submit = async () => { + try { + const activeAccount = await firstValueFrom( + this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)), + ); + const verification: Verification = this.setVerifyDevicesForm.value.verification!; + const request: SetVerifyDevicesRequest = await this.userVerificationService.buildRequest( + verification, + SetVerifyDevicesRequest, + ); + // set verify device opposite what is currently is. + request.verifyDevices = !this.verifyNewDeviceLogin; + await this.accountApiService.setVerifyDevices(request); + await this.accountService.setAccountVerifyNewDeviceLogin( + activeAccount!.id, + request.verifyDevices, + ); + this.dialogRef.close(); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("accountNewDeviceLoginProtectionSaved"), + }); + } catch (e) { + if (e instanceof ErrorResponse && e.statusCode === 400) { + this.invalidSecret = true; + } + throw e; + } + }; + + static open(dialogService: DialogService) { + return dialogService.open(SetAccountVerifyDevicesDialogComponent); + } + + // closes subscription leaks + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 8f0f195440a6..dd5c56f9b915 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -12,10 +12,10 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; +import { getUserId } from "@bitwarden/common/auth/services/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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserId } from "@bitwarden/common/types/guid"; @@ -23,7 +23,6 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { UserKeyRotationService } from "../../key-management/key-rotation/user-key-rotation.service"; @@ -46,8 +45,6 @@ export class ChangePasswordComponent i18nService: I18nService, keyService: KeyService, messagingService: MessagingService, - stateService: StateService, - passwordGenerationService: PasswordGenerationServiceAbstraction, platformUtilsService: PlatformUtilsService, policyService: PolicyService, private auditService: AuditService, @@ -67,10 +64,8 @@ export class ChangePasswordComponent i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, @@ -188,7 +183,7 @@ export class ChangePasswordComponent await this.kdfConfigService.getKdfConfig(), ); - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const newLocalKeyHash = await this.keyService.hashMasterKey( this.masterPassword, newMasterKey, 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 75d43bb3bc76..73191e1539e4 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 @@ -4,7 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang 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 { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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"; 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 73e32add5c22..83bdfffbe4f5 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 @@ -8,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -83,7 +84,8 @@ export class EmergencyAccessComponent implements OnInit { } async ngOnInit() { - const orgs = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); this.isOrganizationOwner = orgs.some((o) => o.isOwner); // 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/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 4e00c962ffdf..5747386cf840 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 @@ -13,9 +13,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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfType, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { EmergencyAccessService } from "../../../emergency-access"; @@ -53,8 +51,6 @@ export class EmergencyAccessTakeoverComponent i18nService: I18nService, keyService: KeyService, messagingService: MessagingService, - stateService: StateService, - passwordGenerationService: PasswordGenerationServiceAbstraction, platformUtilsService: PlatformUtilsService, policyService: PolicyService, private emergencyAccessService: EmergencyAccessService, @@ -70,10 +66,8 @@ export class EmergencyAccessTakeoverComponent i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts index 0ad7eef81be7..c5114c0be6aa 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -43,6 +43,7 @@ describe("EmergencyViewDialogComponent", () => { imports: [EmergencyViewDialogComponent, NoopAnimationsModule], providers: [ { provide: OrganizationService, useValue: mock() }, + { provide: AccountService, useValue: accountService }, { provide: CollectionService, useValue: mock() }, { provide: FolderService, useValue: mock() }, { provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } }, diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index 37827a33afe7..2b8d8be3d0f7 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -5,7 +5,7 @@

{{ "devices" | i18n }}

diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html index 0a2eb346b1bd..c9e2e1114813 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html @@ -14,10 +14,7 @@ {{ "twoStepLoginProviderEnabled" | i18n }} -

{{ "twoFactorWebAuthnWarning" | i18n }}

-
    -
  • {{ "twoFactorWebAuthnSupportWeb" | i18n }}
  • -
+

{{ "twoFactorWebAuthnWarning1" | i18n }}

FIDO2 WebAuthn logo
    diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 3b20718873d9..4530692ebee4 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef } from "@angular/cdk/dialog"; -import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { first, firstValueFrom, @@ -14,7 +14,6 @@ import { } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -67,11 +66,10 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { constructor( protected dialogService: DialogService, protected apiService: ApiService, - protected modalService: ModalService, protected messagingService: MessagingService, protected policyService: PolicyService, billingAccountProfileStateService: BillingAccountProfileStateService, - private accountService: AccountService, + protected accountService: AccountService, ) { this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => @@ -268,13 +266,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { return type === TwoFactorProviderType.OrganizationDuo; } - protected async openModal(ref: ViewContainerRef, type: Type): Promise { - const [modal, childComponent] = await this.modalService.openViewRef(type, ref); - this.modal = modal; - - return childComponent; - } - protected updateStatus(enabled: boolean, type: TwoFactorProviderType) { if (!enabled && this.modal != null) { this.modal.close(); diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts index 48a7f40d238f..c12d7941c444 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts @@ -10,6 +10,8 @@ import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstract import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DialogService } from "@bitwarden/components/src/dialog/dialog.service"; import { WebauthnLoginAdminService } from "../../../core/services/webauthn-login/webauthn-login-admin.service"; diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html deleted file mode 100644 index 077836a7634f..000000000000 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html +++ /dev/null @@ -1,149 +0,0 @@ - - - -
    -

    {{ "createAccount" | i18n }}

    -
    - -
    -
    -
    -
    -
    -
    - Bitwarden - -
    - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -

    - {{ freeTrialText }} -

    - -
    - - - - - - - - - - - - - - -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts deleted file mode 100644 index 61fc7a60035c..000000000000 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts +++ /dev/null @@ -1,336 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { StepperSelectionEvent } from "@angular/cdk/stepper"; -import { TitleCasePipe } from "@angular/common"; -import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; -import { FormBuilder, UntypedFormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { RouterTestingModule } from "@angular/router/testing"; -import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; - -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; -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 { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType } from "@bitwarden/common/billing/enums"; -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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; - -import { RouterService } from "../../core"; -import { SharedModule } from "../../shared"; -import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; -import { OrganizationInvite } from "../organization-invite/organization-invite"; - -import { TrialInitiationComponent } from "./trial-initiation.component"; -import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; - -describe("TrialInitiationComponent", () => { - let component: TrialInitiationComponent; - let fixture: ComponentFixture; - const mockQueryParams = new BehaviorSubject({ org: "enterprise" }); - const testOrgId = "91329456-5b9f-44b3-9279-6bb9ee6a0974"; - const formBuilder: FormBuilder = new FormBuilder(); - let routerSpy: jest.SpyInstance; - - let stateServiceMock: MockProxy; - let policyApiServiceMock: MockProxy; - let policyServiceMock: MockProxy; - let routerServiceMock: MockProxy; - let acceptOrgInviteServiceMock: MockProxy; - let organizationBillingServiceMock: MockProxy; - let configServiceMock: MockProxy; - - beforeEach(() => { - // only define services directly that we want to mock return values in this component - stateServiceMock = mock(); - policyApiServiceMock = mock(); - policyServiceMock = mock(); - routerServiceMock = mock(); - acceptOrgInviteServiceMock = mock(); - organizationBillingServiceMock = mock(); - configServiceMock = mock(); - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - TestBed.configureTestingModule({ - imports: [ - SharedModule, - RouterTestingModule.withRoutes([ - { path: "trial", component: TrialInitiationComponent }, - { - path: `organizations/${testOrgId}/vault`, - component: BlankComponent, - }, - { - path: `organizations/${testOrgId}/members`, - component: BlankComponent, - }, - ]), - ], - declarations: [TrialInitiationComponent, I18nPipe], - providers: [ - UntypedFormBuilder, - { - provide: ActivatedRoute, - useValue: { - queryParams: mockQueryParams.asObservable(), - }, - }, - { provide: StateService, useValue: stateServiceMock }, - { provide: PolicyService, useValue: policyServiceMock }, - { provide: PolicyApiServiceAbstraction, useValue: policyApiServiceMock }, - { provide: LogService, useValue: mock() }, - { provide: I18nService, useValue: mock() }, - { provide: TitleCasePipe, useValue: mock() }, - { - provide: VerticalStepperComponent, - useClass: VerticalStepperStubComponent, - }, - { - provide: RouterService, - useValue: routerServiceMock, - }, - { - provide: AcceptOrganizationInviteService, - useValue: acceptOrgInviteServiceMock, - }, - { - provide: OrganizationBillingService, - useValue: organizationBillingServiceMock, - }, - { - provide: ConfigService, - useValue: configServiceMock, - }, - ], - schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component) - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); - - // These tests demonstrate mocking service calls - describe("onInit() enforcedPolicyOptions", () => { - it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => { - acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null); - // Need to recreate component with new service mock - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - await component.ngOnInit(); - - expect(component.enforcedPolicyOptions).toBe(undefined); - }); - it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => { - // Set up service method mocks - acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({ - organizationId: testOrgId, - token: "token", - email: "testEmail", - organizationUserId: "123", - } as OrganizationInvite); - policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce( - Promise.resolve([ - { - id: "345", - organizationId: testOrgId, - type: 1, - data: { - minComplexity: 4, - minLength: 10, - requireLower: null, - requireNumbers: null, - requireSpecial: null, - requireUpper: null, - }, - enabled: true, - }, - ] as Policy[]), - ); - policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue( - of({ - minComplexity: 4, - minLength: 10, - requireLower: null, - requireNumbers: null, - requireSpecial: null, - requireUpper: null, - } as MasterPasswordPolicyOptions), - ); - - // Need to recreate component with new service mocks - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - await component.ngOnInit(); - expect(component.enforcedPolicyOptions).toMatchObject({ - minComplexity: 4, - minLength: 10, - requireLower: null, - requireNumbers: null, - requireSpecial: null, - requireUpper: null, - }); - }); - }); - - // These tests demonstrate route params - describe("Route params", () => { - it("should set org variable to be enterprise and plan to EnterpriseAnnually if org param is enterprise", fakeAsync(() => { - mockQueryParams.next({ org: "enterprise" }); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - expect(component.org).toBe("enterprise"); - expect(component.plan).toBe(PlanType.EnterpriseAnnually); - })); - it("should not set org variable if no org param is provided", fakeAsync(() => { - mockQueryParams.next({}); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - expect(component.org).toBe(""); - expect(component.accountCreateOnly).toBe(true); - })); - it("should not set the org if org param is invalid ", fakeAsync(async () => { - mockQueryParams.next({ org: "hahahaha" }); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - expect(component.org).toBe(""); - expect(component.accountCreateOnly).toBe(true); - })); - it("should set the layout variable if layout param is valid ", fakeAsync(async () => { - mockQueryParams.next({ layout: "teams1" }); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - expect(component.layout).toBe("teams1"); - expect(component.accountCreateOnly).toBe(false); - })); - it("should not set the layout variable and leave as 'default' if layout param is invalid ", fakeAsync(async () => { - mockQueryParams.next({ layout: "asdfasdf" }); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - // 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 - component.ngOnInit(); - expect(component.layout).toBe("default"); - expect(component.accountCreateOnly).toBe(true); - })); - }); - - // These tests demonstrate the use of a stub component - describe("createAccount()", () => { - beforeEach(() => { - component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent) - .componentInstance as VerticalStepperComponent; - }); - - it("should set email and call verticalStepper.next()", fakeAsync(() => { - const verticalStepperNext = jest.spyOn(component.verticalStepper, "next"); - component.createdAccount("test@email.com"); - expect(verticalStepperNext).toHaveBeenCalled(); - expect(component.email).toBe("test@email.com"); - })); - }); - - describe("billingSuccess()", () => { - beforeEach(() => { - component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent) - .componentInstance as VerticalStepperComponent; - }); - - it("should set orgId and call verticalStepper.next()", () => { - const verticalStepperNext = jest.spyOn(component.verticalStepper, "next"); - component.billingSuccess({ orgId: testOrgId }); - expect(verticalStepperNext).toHaveBeenCalled(); - expect(component.orgId).toBe(testOrgId); - }); - }); - - describe("stepSelectionChange()", () => { - beforeEach(() => { - component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent) - .componentInstance as VerticalStepperComponent; - }); - - it("on step 2 should show organization copy text", () => { - component.stepSelectionChange({ - selectedIndex: 1, - previouslySelectedIndex: 0, - } as StepperSelectionEvent); - - expect(component.orgInfoSubLabel).toContain("Enter your"); - expect(component.orgInfoSubLabel).toContain(" organization information"); - }); - it("going from step 2 to 3 should set the orgInforSubLabel to be the Org name from orgInfoFormGroup", () => { - component.orgInfoFormGroup = formBuilder.group({ - name: ["Hooli"], - email: [""], - }); - component.stepSelectionChange({ - selectedIndex: 2, - previouslySelectedIndex: 1, - } as StepperSelectionEvent); - - expect(component.orgInfoSubLabel).toContain("Hooli"); - }); - }); - - describe("previousStep()", () => { - beforeEach(() => { - component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent) - .componentInstance as VerticalStepperComponent; - }); - - it("should call verticalStepper.previous()", fakeAsync(() => { - const verticalStepperPrevious = jest.spyOn(component.verticalStepper, "previous"); - component.previousStep(); - expect(verticalStepperPrevious).toHaveBeenCalled(); - })); - }); - - // These tests demonstrate router navigation - describe("navigation methods", () => { - beforeEach(() => { - component.orgId = testOrgId; - const router = TestBed.inject(Router); - fixture.detectChanges(); - routerSpy = jest.spyOn(router, "navigate"); - }); - describe("navigateToOrgVault", () => { - it("should call verticalStepper.previous()", fakeAsync(() => { - component.navigateToOrgVault(); - expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "vault"]); - })); - }); - describe("navigateToOrgVault", () => { - it("should call verticalStepper.previous()", fakeAsync(() => { - component.navigateToOrgInvite(); - expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "members"]); - })); - }); - }); -}); - -export class VerticalStepperStubComponent extends VerticalStepperComponent {} -export class BlankComponent {} // For router tests diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts deleted file mode 100644 index fbe3eb7aa6db..000000000000 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts +++ /dev/null @@ -1,353 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { StepperSelectionEvent } from "@angular/cdk/stepper"; -import { TitleCasePipe } from "@angular/common"; -import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { UntypedFormBuilder, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -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 { - OrganizationInformation, - PlanInformation, - OrganizationBillingServiceAbstraction as OrganizationBillingService, -} from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -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 { - OrganizationCreatedEvent, - SubscriptionProduct, - TrialOrganizationType, -} from "../../billing/accounts/trial-initiation/trial-billing-step.component"; -import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; -import { OrganizationInvite } from "../organization-invite/organization-invite"; - -import { RouterService } from "./../../core/router.service"; -import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; - -export enum ValidOrgParams { - families = "families", - enterprise = "enterprise", - teams = "teams", - teamsStarter = "teamsStarter", - individual = "individual", - premium = "premium", - free = "free", -} - -enum ValidLayoutParams { - default = "default", - teams = "teams", - teams1 = "teams1", - teams2 = "teams2", - teams3 = "teams3", - enterprise = "enterprise", - enterprise1 = "enterprise1", - enterprise2 = "enterprise2", - cnetcmpgnent = "cnetcmpgnent", - cnetcmpgnind = "cnetcmpgnind", - cnetcmpgnteams = "cnetcmpgnteams", - abmenterprise = "abmenterprise", - abmteams = "abmteams", - secretsManager = "secretsManager", -} - -@Component({ - selector: "app-trial", - templateUrl: "trial-initiation.component.html", -}) -export class TrialInitiationComponent implements OnInit, OnDestroy { - email = ""; - fromOrgInvite = false; - org = ""; - orgInfoSubLabel = ""; - orgId = ""; - orgLabel = ""; - billingSubLabel = ""; - layout = "default"; - plan: PlanType; - productTier: ProductTierType; - accountCreateOnly = true; - useTrialStepper = false; - loading = false; - policies: Policy[]; - enforcedPolicyOptions: MasterPasswordPolicyOptions; - trialFlowOrgs: string[] = [ - ValidOrgParams.teams, - ValidOrgParams.teamsStarter, - ValidOrgParams.enterprise, - ValidOrgParams.families, - ]; - routeFlowOrgs: string[] = [ - ValidOrgParams.free, - ValidOrgParams.premium, - ValidOrgParams.individual, - ]; - layouts = ValidLayoutParams; - referenceData: ReferenceEventRequest; - @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; - - orgInfoFormGroup = this.formBuilder.group({ - name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }], - email: [""], - }); - - private set referenceDataId(referenceId: string) { - if (referenceId != null) { - this.referenceData.id = referenceId; - } else { - this.referenceData.id = ("; " + document.cookie) - .split("; reference=") - .pop() - .split(";") - .shift(); - } - - if (this.referenceData.id === "") { - this.referenceData.id = null; - } else { - // Matches "_ga_QBRN562QQQ=value1.value2.session" and captures values and session. - const regex = /_ga_QBRN562QQQ=([^.]+)\.([^.]+)\.(\d+)/; - const match = document.cookie.match(regex); - if (match) { - this.referenceData.session = match[3]; - } - } - } - - private destroy$ = new Subject(); - protected enableTrialPayment$ = this.configService.getFeatureFlag$( - FeatureFlag.TrialPaymentOptional, - ); - - constructor( - private route: ActivatedRoute, - protected router: Router, - private formBuilder: UntypedFormBuilder, - private titleCasePipe: TitleCasePipe, - private logService: LogService, - private policyApiService: PolicyApiServiceAbstraction, - private policyService: PolicyService, - private i18nService: I18nService, - private routerService: RouterService, - private acceptOrgInviteService: AcceptOrganizationInviteService, - private organizationBillingService: OrganizationBillingService, - private configService: ConfigService, - ) {} - - async ngOnInit(): Promise { - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => { - this.referenceData = new ReferenceEventRequest(); - if (qParams.email != null && qParams.email.indexOf("@") > -1) { - this.email = qParams.email; - this.fromOrgInvite = qParams.fromOrgInvite === "true"; - } - - this.referenceDataId = qParams.reference; - - if (Object.values(ValidLayoutParams).includes(qParams.layout)) { - this.layout = qParams.layout; - this.accountCreateOnly = false; - } - - if (this.trialFlowOrgs.includes(qParams.org)) { - this.org = qParams.org; - this.orgLabel = this.titleCasePipe.transform(this.orgDisplayName); - this.useTrialStepper = true; - this.referenceData.flow = qParams.org; - - if (this.org === ValidOrgParams.families) { - this.plan = PlanType.FamiliesAnnually; - this.productTier = ProductTierType.Families; - } else if (this.org === ValidOrgParams.teamsStarter) { - this.plan = PlanType.TeamsStarter; - this.productTier = ProductTierType.TeamsStarter; - } else if (this.org === ValidOrgParams.teams) { - this.plan = PlanType.TeamsAnnually; - this.productTier = ProductTierType.Teams; - } else if (this.org === ValidOrgParams.enterprise) { - this.plan = PlanType.EnterpriseAnnually; - this.productTier = ProductTierType.Enterprise; - } - } else if (this.routeFlowOrgs.includes(qParams.org)) { - this.referenceData.flow = qParams.org; - const route = this.router.createUrlTree(["create-organization"], { - queryParams: { plan: qParams.org }, - }); - this.routerService.setPreviousUrl(route.toString()); - } - - // Are they coming from an email for sponsoring a families organization - // After logging in redirect them to setup the families sponsorship - this.setupFamilySponsorship(qParams.sponsorshipToken); - - this.referenceData.initiationPath = this.accountCreateOnly - ? "Registration form" - : "Password Manager trial from marketing website"; - }); - - // If there's a deep linked org invite, use it to get the password policies - const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); - if (orgInvite != null) { - await this.initPasswordPolicies(orgInvite); - } - - this.orgInfoFormGroup.controls.name.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.orgInfoFormGroup.controls.name.markAsTouched(); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - stepSelectionChange(event: StepperSelectionEvent) { - // Set org info sub label - if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") { - this.orgInfoSubLabel = - "Enter your " + - this.titleCasePipe.transform(this.orgDisplayName) + - " organization information"; - } else if (event.previouslySelectedIndex === 1) { - this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value; - } - - //set billing sub label - if (event.selectedIndex === 2) { - this.billingSubLabel = this.i18nService.t("billingTrialSubLabel"); - } - } - - async createOrganizationOnTrial() { - this.loading = true; - const organization: OrganizationInformation = { - name: this.orgInfoFormGroup.get("name").value, - billingEmail: this.orgInfoFormGroup.get("email").value, - initiationPath: "Password Manager trial from marketing website", - }; - - const plan: PlanInformation = { - type: this.plan, - passwordManagerSeats: 1, - }; - - const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ - organization, - plan, - }); - - this.orgId = response?.id; - this.billingSubLabel = `${this.i18nService.t("annual")} ($0/${this.i18nService.t("yr")})`; - this.loading = false; - this.verticalStepper.next(); - } - - createdAccount(email: string) { - this.email = email; - this.orgInfoFormGroup.get("email")?.setValue(email); - this.verticalStepper.next(); - } - - billingSuccess(event: any) { - this.orgId = event?.orgId; - this.billingSubLabel = event?.subLabelText; - this.verticalStepper.next(); - } - - createdOrganization(event: OrganizationCreatedEvent) { - this.orgId = event.organizationId; - this.billingSubLabel = event.planDescription; - this.verticalStepper.next(); - } - - navigateToOrgVault() { - // 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", this.orgId, "vault"]); - } - - navigateToOrgInvite() { - // 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", this.orgId, "members"]); - } - - previousStep() { - this.verticalStepper.previous(); - } - - get orgDisplayName() { - if (this.org === "teamsStarter") { - return "Teams Starter"; - } - - return this.org; - } - - get freeTrialText() { - const translationKey = - this.layout === this.layouts.secretsManager - ? "startYour7DayFreeTrialOfBitwardenSecretsManagerFor" - : "startYour7DayFreeTrialOfBitwardenFor"; - - return this.i18nService.t(translationKey, this.org); - } - - get trialOrganizationType(): TrialOrganizationType { - switch (this.productTier) { - case ProductTierType.Free: - return null; - default: - return this.productTier; - } - } - - private setupFamilySponsorship(sponsorshipToken: string) { - if (sponsorshipToken != null) { - const route = this.router.createUrlTree(["setup/families-for-enterprise"], { - queryParams: { plan: sponsorshipToken }, - }); - this.routerService.setPreviousUrl(route.toString()); - } - } - - private async initPasswordPolicies(invite: OrganizationInvite): Promise { - if (invite == null) { - return; - } - - try { - this.policies = await this.policyApiService.getPoliciesByToken( - invite.organizationId, - invite.token, - invite.email, - invite.organizationUserId, - ); - } catch (e) { - this.logService.error(e); - } - - if (this.policies != null) { - this.policyService - .masterPasswordPolicyOptions$(this.policies) - .pipe(takeUntil(this.destroy$)) - .subscribe((enforcedPasswordPolicyOptions) => { - this.enforcedPolicyOptions = enforcedPasswordPolicyOptions; - }); - } - } - - protected readonly SubscriptionProduct = SubscriptionProduct; -} diff --git a/apps/web/src/app/auth/two-factor-auth-duo.component.ts b/apps/web/src/app/auth/two-factor-auth-duo.component.ts index b82632008bd3..971c1f3764c4 100644 --- a/apps/web/src/app/auth/two-factor-auth-duo.component.ts +++ b/apps/web/src/app/auth/two-factor-auth-duo.component.ts @@ -8,11 +8,23 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; @Component({ diff --git a/apps/web/src/app/auth/two-factor-auth.component.ts b/apps/web/src/app/auth/two-factor-auth.component.ts index 18660b2ca631..cbe1d8f0a53f 100644 --- a/apps/web/src/app/auth/two-factor-auth.component.ts +++ b/apps/web/src/app/auth/two-factor-auth.component.ts @@ -25,19 +25,39 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthEmailComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthWebAuthnComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthYubikeyComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorOptionsComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "../../../../../libs/auth/src/common/abstractions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html index 2f983944b707..361b2d9a3a34 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html @@ -19,7 +19,7 @@

    {{ "billingPlanLabel" | i18n }

-

+

{{ "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount }} @@ -49,49 +49,58 @@

{{ "goPremium" | i18n }}

linkType="primary" routerLink="/create-organization" [queryParams]="{ plan: 'families' }" - >{{ "bitwardenFamiliesPlan" | i18n }} + {{ "bitwardenFamiliesPlan" | i18n }} +

{{ "purchasePremium" | i18n }}
- -

{{ "uploadLicenseFilePremium" | i18n }}

-
- - {{ "licenseFile" | i18n }} -
- - {{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }} -
- - {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} -
- -
+ + +

{{ "uploadLicenseFilePremium" | i18n }}

+
+ + {{ "licenseFile" | i18n }} +
+ + {{ + licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n) + }} +
+ + {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} +
+ +
+
+
-
+

{{ "addons" | i18n }}

@@ -106,7 +115,7 @@

{{ "addons" | i18n }}

/> {{ "additionalStorageIntervalDesc" - | i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n) + | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) }}
@@ -114,30 +123,26 @@

{{ "addons" | i18n }}

{{ "summary" | i18n }}

{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = - {{ additionalStorageTotal | currency: "$" }} + {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × + {{ storageGBPrice | currency: "$" }} = + {{ additionalStorageCost | currency: "$" }}

{{ "paymentInformation" | i18n }}

- - -
-
- {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} -
- - {{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }} - + + +
+
+ {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} + {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}
-
-

- {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} -

-

{{ "paymentChargedAnnually" | i18n }}

- diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index f96f573cd4d6..ec19eb025945 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -1,187 +1,197 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Component, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; -import { firstValueFrom, Observable, switchMap } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs"; import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; +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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { ToastService } from "@bitwarden/components"; -import { PaymentComponent, TaxInfoComponent } from "../../shared"; +import { PaymentComponent } from "../../shared/payment/payment.component"; +import { TaxInfoComponent } from "../../shared/tax-info.component"; @Component({ - templateUrl: "premium.component.html", + templateUrl: "./premium.component.html", }) -export class PremiumComponent implements OnInit { +export class PremiumComponent { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; - canAccessPremium$: Observable; - selfHosted = false; - premiumPrice = 10; - familyPlanMaxUserCount = 6; - storageGbPrice = 4; - cloudWebVaultUrl: string; - licenseFile: File = null; - - formPromise: Promise; - protected licenseForm = new FormGroup({ - file: new FormControl(null, [Validators.required]), + protected hasPremiumFromAnyOrganization$: Observable; + + protected addOnFormGroup = new FormGroup({ + additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), }); - protected addonForm = new FormGroup({ - additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]), + + protected licenseFormGroup = new FormGroup({ + file: new FormControl(null, [Validators.required]), }); - private estimatedTax: number = 0; + protected cloudWebVaultURL: string; + protected isSelfHost = false; + + protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( + FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, + ); + + protected estimatedTax: number = 0; + protected readonly familyPlanMaxUserCount = 6; + protected readonly premiumPrice = 10; + protected readonly storageGBPrice = 4; constructor( + private activatedRoute: ActivatedRoute, private apiService: ApiService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private configService: ConfigService, + private environmentService: EnvironmentService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private tokenService: TokenService, private router: Router, - private messagingService: MessagingService, private syncService: SyncService, - private environmentService: EnvironmentService, - private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, + private tokenService: TokenService, private taxService: TaxServiceAbstraction, private accountService: AccountService, ) { - this.selfHosted = platformUtilsService.isSelfHost(); - this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + this.isSelfHost = this.platformUtilsService.isSelfHost(); + + this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id), ), ); - this.addonForm.controls.additionalStorage.valueChanges + combineLatest([ + this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumPersonally$(account.id), + ), + ), + this.environmentService.cloudWebVaultUrl$, + ]) + .pipe( + takeUntilDestroyed(), + concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => { + if (hasPremiumPersonally) { + return from(this.navigateToSubscriptionPage()); + } + + this.cloudWebVaultURL = cloudWebVaultURL; + return of(true); + }), + ) + .subscribe(); + + this.addOnFormGroup.controls.additionalStorage.valueChanges .pipe(debounceTime(1000), takeUntilDestroyed()) .subscribe(() => { this.refreshSalesTax(); }); } - protected setSelectedFile(event: Event) { - const fileInputEl = event.target; - const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; - this.licenseFile = file; - } - async ngOnInit() { - this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); - const account = await firstValueFrom(this.accountService.activeAccount$); - if ( - await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(account.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.router.navigate(["/settings/subscription/user-subscription"]); - return; - } - } - submit = async () => { - if (this.taxInfoComponent) { - if (!this.taxInfoComponent?.taxFormGroup.valid) { - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - return; - } - } - this.licenseForm.markAllAsTouched(); - this.addonForm.markAllAsTouched(); - if (this.selfHosted) { - if (this.licenseFile == null) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("selectFile"), - }); - return; - } - } - - if (this.selfHosted) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - if (!this.tokenService.getEmailVerified()) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("verifyEmailFirst"), - }); - return; - } - - const fd = new FormData(); - fd.append("license", this.licenseFile); - await this.apiService.postAccountLicense(fd).then(() => { - return this.finalizePremium(); - }); - } else { - await this.paymentComponent - .createPaymentToken() - .then((result) => { - const fd = new FormData(); - fd.append("paymentMethodType", result[1].toString()); - if (result[0] != null) { - fd.append("paymentToken", result[0]); - } - fd.append("additionalStorageGb", (this.additionalStorage || 0).toString()); - fd.append("country", this.taxInfoComponent?.taxFormGroup?.value.country); - fd.append("postalCode", this.taxInfoComponent?.taxFormGroup?.value.postalCode); - return this.apiService.postPremium(fd); - }) - .then((paymentResponse) => { - if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) { - return this.paymentComponent.handleStripeCardPayment( - paymentResponse.paymentIntentClientSecret, - () => this.finalizePremium(), - ); - } else { - return this.finalizePremium(); - } - }); - } - }; - async finalizePremium() { + finalizeUpgrade = async () => { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); + }; + + postFinalizeUpgrade = async () => { this.toastService.showToast({ variant: "success", title: null, message: this.i18nService.t("premiumUpdated"), }); - await this.router.navigate(["/settings/subscription/user-subscription"]); - } + await this.navigateToSubscriptionPage(); + }; + + navigateToSubscriptionPage = (): Promise => + this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); + + onLicenseFileSelected = (event: Event): void => { + const element = event.target as HTMLInputElement; + this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; + }; + + submitPremiumLicense = async (): Promise => { + this.licenseFormGroup.markAllAsTouched(); + + if (this.licenseFormGroup.invalid) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); + } + + const emailVerified = await this.tokenService.getEmailVerified(); + if (!emailVerified) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verifyEmailFirst"), + }); + } - get additionalStorage(): number { - return this.addonForm.get("additionalStorage").value; + const formData = new FormData(); + formData.append("license", this.licenseFormGroup.value.file); + + await this.apiService.postAccountLicense(formData); + await this.finalizeUpgrade(); + await this.postFinalizeUpgrade(); + }; + + submitPayment = async (): Promise => { + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + if (this.taxInfoComponent.taxFormGroup.invalid) { + return; + } + + const { type, token } = await this.paymentComponent.tokenize(); + + const formData = new FormData(); + formData.append("paymentMethodType", type.toString()); + formData.append("paymentToken", token); + formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); + formData.append("country", this.taxInfoComponent.country); + formData.append("postalCode", this.taxInfoComponent.postalCode); + + await this.apiService.postPremium(formData); + await this.finalizeUpgrade(); + await this.postFinalizeUpgrade(); + }; + + protected get additionalStorageCost(): number { + return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; } - get additionalStorageTotal(): number { - return this.storageGbPrice * Math.abs(this.additionalStorage || 0); + + protected get premiumURL(): string { + return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; } - get subtotal(): number { - return this.premiumPrice + this.additionalStorageTotal; + protected get subtotal(): number { + return this.premiumPrice + this.additionalStorageCost; } - get taxCharges(): number { - return this.estimatedTax; + protected get total(): number { + return this.subtotal + this.estimatedTax; } - get total(): number { - return this.subtotal + this.taxCharges || 0; + protected async onLicenseFileSelectedChanged(): Promise { + await this.postFinalizeUpgrade(); } private refreshSalesTax(): void { @@ -190,7 +200,7 @@ export class PremiumComponent implements OnInit { } const request: PreviewIndividualInvoiceRequest = { passwordManager: { - additionalStorage: this.addonForm.value.additionalStorage, + additionalStorage: this.addOnFormGroup.value.additionalStorage, }, taxInformation: { postalCode: this.taxInfoComponent.postalCode, 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 97b4725e6d7a..38f4436fb475 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -8,8 +8,6 @@ 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 { 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"; @@ -18,12 +16,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { DialogService, ToastService } from "@bitwarden/components"; import { - AdjustStorageDialogV2Component, - AdjustStorageDialogV2ResultType, -} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; -import { - AdjustStorageDialogResult, - openAdjustStorageDialog, + AdjustStorageDialogComponent, + AdjustStorageDialogResultType, } from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, @@ -45,10 +39,6 @@ export class UserSubscriptionComponent implements OnInit { cancelPromise: Promise; reinstatePromise: Promise; - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -60,7 +50,6 @@ export class UserSubscriptionComponent implements OnInit { private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, - private configService: ConfigService, private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); @@ -166,33 +155,18 @@ export class UserSubscriptionComponent implements OnInit { }; adjustStorage = async (add: boolean) => { - const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); - - if (deprecateStripeSourcesAPI) { - const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { - data: { - price: 4, - cadence: "year", - type: add ? "Add" : "Remove", - }, - }); + const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { + data: { + price: 4, + cadence: "year", + type: add ? "Add" : "Remove", + }, + }); - const result = await lastValueFrom(dialogRef.closed); + 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(); - } + if (result === AdjustStorageDialogResultType.Submitted) { + await this.load(); } }; 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 0166560007e4..786c25c8d4cb 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -2,11 +2,16 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + InternalOrganizationServiceAbstraction, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; @@ -37,6 +42,7 @@ export class AdjustSubscription implements OnInit, OnDestroy { private formBuilder: FormBuilder, private toastService: ToastService, private internalOrganizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -73,14 +79,19 @@ export class AdjustSubscription implements OnInit, OnDestroy { request, ); - const organization = await this.internalOrganizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.internalOrganizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const organizationData = new OrganizationData(response, { isMember: organization.isMember, isProviderUser: organization.isProviderUser, }); - await this.internalOrganizationService.upsert(organizationData); + await this.internalOrganizationService.upsert(organizationData, userId); this.toastService.showToast({ variant: "success", 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 index 89c0075ba0d3..ca1b9245c0bb 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -118,7 +118,13 @@ ) | currency: "$" }} - /{{ "monthPerMember" | i18n }} + + /{{ + selectableProduct.productTier === productTypes.Families + ? "month" + : ("monthPerMember" | i18n) + }} @@ -346,25 +352,20 @@

{{ "paymentMethod" | i18n }}

" > - {{ - deprecateStripeSourcesAPI - ? paymentSource?.description - : billing?.paymentSource?.description - }} + {{ paymentSource?.description }} {{ "changePaymentMethod" | i18n }}

- - + -
+

{{ "total" | i18n }}: @@ -400,7 +401,7 @@

{{ "paymentMethod" | i18n }}

: selectedPlan.PasswordManager.basePrice ) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }} @@ -427,7 +428,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}
@@ -444,7 +445,7 @@

{{ "paymentMethod" | i18n }}

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

@@ -485,7 +486,7 @@

{{ "paymentMethod" | i18n }}

: selectedPlan.SecretsManager.basePrice ) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}

{{ "paymentMethod" | i18n }} {{ "members" | i18n }} × {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }} @@ -524,7 +525,7 @@

{{ "paymentMethod" | i18n }}

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

@@ -580,7 +581,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }} {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} @@ -596,7 +597,7 @@

{{ "paymentMethod" | i18n }}

{{ "additionalStorageGbMessage" | i18n }} × {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ storageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb | currency: "$" @@ -656,7 +657,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" }} @@ -675,7 +676,7 @@

{{ "paymentMethod" | i18n }}

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

@@ -725,7 +726,7 @@

{{ "paymentMethod" | i18n }}

: selectedPlan.SecretsManager.basePrice ) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}

{{ "paymentMethod" | i18n }} {{ "members" | i18n }} × {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }} @@ -764,7 +765,7 @@

{{ "paymentMethod" | i18n }}

{{ "serviceAccounts" | i18n }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }} {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

@@ -786,7 +787,7 @@

{{ "paymentMethod" | i18n }}

: selectedPlan.PasswordManager.basePrice ) | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }} @@ -813,7 +814,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ "year" | i18n }} + /{{ selectedPlanInterval | i18n }}
@@ -860,7 +861,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }}
{{ secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" }} @@ -879,7 +880,7 @@

{{ "paymentMethod" | i18n }}

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

@@ -914,7 +915,7 @@

{{ "paymentMethod" | i18n }}

{{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} - /{{ "month" | i18n }} + /{{ selectedPlanInterval | i18n }} {{ "freeForOneYear" | i18n }} @@ -962,7 +963,7 @@

{{ "paymentMethod" | i18n }}

-
+

; isSubscriptionCanceled: boolean = false; + secretsManagerTotal: number; private destroy$ = new Subject(); @@ -205,17 +204,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private accountService: AccountService, private organizationBillingService: OrganizationBillingService, ) {} 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 = @@ -225,21 +220,24 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.organizationId = this.dialogParams.organizationId; this.currentPlan = this.sub?.plan; this.selectedPlan = this.sub?.plan; - this.organization = await this.organizationService.get(this.organizationId); - if (this.deprecateStripeSourcesAPI) { - const { accountCredit, paymentSource } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; - } else { - this.billing = await this.organizationApiService.getBilling(this.organizationId); - } + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); + const { accountCredit, paymentSource } = + await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); + this.accountCredit = accountCredit; + this.paymentSource = paymentSource; } 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); + this.plans = await this.apiService.getPlans(); + this.passwordManagerPlans = this.plans.data.filter((plan) => !!plan.PasswordManager); + this.secretsManagerPlans = this.plans.data.filter((plan) => !!plan.SecretsManager); if ( this.productTier === ProductTierType.Enterprise || @@ -292,7 +290,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); this.taxInformation = TaxInformation.from(taxInfo); - this.refreshSalesTax(); + if (!this.isSubscriptionCanceled) { + this.refreshSalesTax(); + } } resolveHeaderName(subscription: OrganizationSubscriptionResponse): string { @@ -320,16 +320,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.selectableProducts.find((product) => product.productTier === productTier); } - secretsManagerTrialDiscount() { - return this.sub?.customerDiscount?.appliesTo?.includes("sm-standalone") - ? this.discountPercentage - : this.discountPercentageFromSub + this.discountPercentage; - } - isPaymentSourceEmpty() { - return this.deprecateStripeSourcesAPI - ? this.paymentSource === null || this.paymentSource === undefined - : this.billing?.paymentSource === null || this.billing?.paymentSource === undefined; + return this.paymentSource === null || this.paymentSource === undefined; } isSecretsManagerTrial(): boolean { @@ -473,18 +465,23 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { get upgradeRequiresPaymentMethod() { const isFreeTier = this.organization?.productTierType === ProductTierType.Free; const shouldHideFree = !this.showFree; - const hasNoPaymentSource = this.deprecateStripeSourcesAPI - ? !this.paymentSource - : !this.billing?.paymentSource; + const hasNoPaymentSource = !this.paymentSource; return isFreeTier && shouldHideFree && hasNoPaymentSource; } get selectedSecretsManagerPlan() { - return this.secretsManagerPlans.find((plan) => plan.type === this.selectedPlan.type); + let planResponse: PlanResponse; + if (this.secretsManagerPlans) { + return this.secretsManagerPlans.find((plan) => plan.type === this.selectedPlan.type); + } + return planResponse; } get selectedPlanInterval() { + if (this.isSubscriptionCanceled) { + return this.currentPlan.isAnnual ? "year" : "month"; + } return this.selectedPlan.isAnnual ? "year" : "month"; } @@ -619,18 +616,19 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return subTotal - this.discount; } - get secretsManagerSubtotal() { + secretsManagerSubtotal() { + this.secretsManagerTotal = 0; const plan = this.selectedSecretsManagerPlan; if (!this.organization.useSecretsManager) { - return 0; + return this.secretsManagerTotal; } - return ( + this.secretsManagerTotal = plan.SecretsManager.basePrice + this.secretsManagerSeatTotal(plan, this.sub?.smSeats) + - this.additionalServiceAccountTotal(plan) - ); + this.additionalServiceAccountTotal(plan); + return this.secretsManagerTotal; } get passwordManagerSeats() { @@ -641,11 +639,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get total() { - if (this.organization.useSecretsManager) { + if (this.organization && this.organization.useSecretsManager) { return ( this.passwordManagerSubtotal + this.additionalStorageTotal(this.selectedPlan) + - this.secretsManagerSubtotal + + this.secretsManagerSubtotal() + this.estimatedTax ); } @@ -705,25 +703,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } changedCountry() { - if (this.deprecateStripeSourcesAPI && this.paymentV2Component) { - this.paymentV2Component.showBankAccount = this.taxInformation.country === "US"; + this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else if (this.paymentComponent && this.taxInformation) { - this.paymentComponent!.hideBank = this.taxInformation.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.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -788,8 +774,15 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { billingEmail: org.billingEmail, }; + const filteredPlan = this.plans.data + .filter((plan) => plan.productTier === this.selectedPlan.productTier && !plan.legacyYear) + .find((plan) => { + const isSameBillingCycle = plan.isAnnual === this.selectedPlan.isAnnual; + return isSameBillingCycle; + }); + const plan: PlanInformation = { - type: this.selectedPlan.type, + type: filteredPlan.type, passwordManagerSeats: org.seats, }; @@ -798,14 +791,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { plan.secretsManagerSeats = org.smSeats; } - let paymentMethod: [string, PaymentMethodType]; - - if (this.deprecateStripeSourcesAPI) { - const { type, token } = await this.paymentV2Component.tokenize(); - paymentMethod = [token, type]; - } else { - paymentMethod = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); + const paymentMethod: [string, PaymentMethodType] = [token, type]; const payment: PaymentInformation = { paymentMethod, @@ -841,27 +828,17 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { - if (this.deprecateStripeSourcesAPI) { - const tokenizedPaymentSource = await this.paymentV2Component.tokenize(); - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - - 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.taxInformation.country; - paymentRequest.postalCode = this.taxInformation.postalCode; - await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); - } + const tokenizedPaymentSource = await this.paymentComponent.tokenize(); + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; + updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( + this.taxInformation, + ); + + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); } // Backfill pub/priv key if necessary @@ -871,10 +848,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { 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); - } + await this.organizationApiService.upgrade(this.organizationId, request); return this.organizationId; } @@ -971,38 +945,20 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get paymentSourceClasses() { - if (this.deprecateStripeSourcesAPI) { - 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 []; - } - } else { - if (this.billing.paymentSource == null) { + 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 []; - } - 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 []; - } } } @@ -1097,10 +1053,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.estimatedTax = invoice.taxAmount; }) .catch((error) => { + const translatedMessage = this.i18nService.t(error.message); this.toastService.showToast({ title: "", variant: "error", - message: this.i18nService.t(error.message), + message: + !translatedMessage || translatedMessage === "" ? error.message : translatedMessage, }); }); } 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 3d4c8dd3870d..1bfb9fc4912c 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,14 +1,11 @@ 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"; import { WebPlatformUtilsService } from "../../core/web-platform-utils.service"; -import { PaymentMethodComponent } from "../shared"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; @@ -28,21 +25,17 @@ const routes: Routes = [ : OrganizationSubscriptionCloudComponent, data: { titleId: "subscription" }, }, - ...featureFlaggedRoute({ - defaultComponent: PaymentMethodComponent, - flaggedComponent: OrganizationPaymentMethodComponent, - featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - routeOptions: { - path: "payment-method", - canActivate: [ - organizationPermissionsGuard((org) => org.canEditPaymentMethods), - organizationIsUnmanaged, - ], - data: { - titleId: "paymentMethod", - }, + { + path: "payment-method", + component: OrganizationPaymentMethodComponent, + 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 48ac613711da..d8f4b7393aa7 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -1,5 +1,7 @@ import { NgModule } from "@angular/core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; import { LooseComponentsModule } from "../../shared"; 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 d37f95e3aa2e..0a4eea57f922 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -434,12 +434,10 @@

{{ paymentDesc }}

- + *ngIf="createOrganization || upgradeRequiresPaymentMethod" + [showAccountCredit]="false" + > + }}

- - - 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 edc29b160491..071d1f751611 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -11,13 +11,16 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { debounceTime, map } from "rxjs/operators"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -27,20 +30,20 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-organization-create.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; @@ -53,7 +56,6 @@ import { KeyService } from "@bitwarden/key-management"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; -import { PaymentV2Component } from "../shared/payment/payment-v2.component"; import { PaymentComponent } from "../shared/payment/payment.component"; interface OnSuccessArgs { @@ -75,7 +77,6 @@ const Allowed2020PlansForLegacyProviders = [ }) export class OrganizationPlansComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component; @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent; @Input() organizationId?: string; @@ -124,7 +125,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { singleOrgPolicyAppliesToActiveUser = false; isInTrialFlow = false; discount = 0; - deprecateStripeSourcesAPI: boolean; protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, @@ -179,17 +179,21 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); } async ngOnInit() { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - if (this.organizationId) { - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); this.billing = await this.organizationApiService.getBilling(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.taxInformation = await this.organizationApiService.getTaxInfo(this.organizationId); @@ -568,23 +572,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } protected changedCountry(): void { - if (this.deprecateStripeSourcesAPI) { - this.paymentV2Component.showBankAccount = this.taxInformation?.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else { - this.paymentComponent.hideBank = this.taxInformation?.country !== "US"; - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + this.paymentComponent.showBankAccount = this.taxInformation?.country === "US"; + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -605,6 +598,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { submit = async () => { if (this.taxComponent && !this.taxComponent.validate()) { + this.taxComponent.markAllAsTouched(); return; } @@ -738,25 +732,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod) { - if (this.deprecateStripeSourcesAPI) { - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = await this.paymentV2Component.tokenize(); - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); - } else { - const [paymentToken, paymentMethodType] = await this.paymentComponent.createPaymentToken(); - const paymentRequest = new PaymentRequest(); - paymentRequest.paymentToken = paymentToken; - paymentRequest.paymentMethodType = paymentMethodType; - paymentRequest.country = this.taxInformation?.country; - paymentRequest.postalCode = this.taxInformation?.postalCode; - await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); - } + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = await this.paymentComponent.tokenize(); + updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( + this.taxInformation, + ); + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); } // Backfill pub/priv key if necessary @@ -766,10 +750,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { 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); - } + await this.organizationApiService.upgrade(this.organizationId, request); return this.organizationId; } @@ -790,14 +771,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { - let type: PaymentMethodType; - let token: string; - - if (this.deprecateStripeSourcesAPI) { - ({ type, token } = await this.paymentV2Component.tokenize()); - } else { - [token, type] = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); request.paymentToken = token; request.paymentMethodType = type; 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 0cd21d0f688c..1d8a7846d9dc 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 @@ -294,6 +294,10 @@

{{ "manageSubscription" | i18n }}

+ +

{{ "manageSubscription" | i18n }}

+

{{ resellerSeatsRemainingMessage }}

+

{{ "selfHostingTitleProper" | i18n }} diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 0805e92ee2a3..50c755af63bd 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -4,11 +4,20 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs"; +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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + OrganizationApiKeyType, + OrganizationUserStatusType, +} from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -20,12 +29,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { DialogService, ToastService } from "@bitwarden/components"; import { - AdjustStorageDialogV2Component, - AdjustStorageDialogV2ResultType, -} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; -import { - AdjustStorageDialogResult, - openAdjustStorageDialog, + AdjustStorageDialogComponent, + AdjustStorageDialogResultType, } from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, @@ -50,7 +55,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy organizationId: string; userOrg: Organization; showChangePlan = false; - showDownloadLicense = false; hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; @@ -61,27 +65,28 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy showSubscription = true; showSelfHost = false; organizationIsManagedByConsolidatedBillingMSP = false; + resellerSeatsRemainingMessage: string; protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon; protected readonly teamsStarter = ProductTierType.TeamsStarter; - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - private destroy$ = new Subject(); + private seatsRemainingMessage: string; + constructor( private apiService: ApiService, private i18nService: I18nService, private logService: LogService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private route: ActivatedRoute, private dialogService: DialogService, private configService: ConfigService, private toastService: ToastService, private billingApiService: BillingApiServiceAbstraction, + private organizationUserApiService: OrganizationUserApiService, ) {} async ngOnInit() { @@ -107,6 +112,28 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } } } + + if (this.userOrg.hasReseller) { + const allUsers = await this.organizationUserApiService.getAllUsers(this.userOrg.id); + + const userCount = allUsers.data.filter((user) => + [ + OrganizationUserStatusType.Invited, + OrganizationUserStatusType.Accepted, + OrganizationUserStatusType.Confirmed, + ].includes(user.status), + ).length; + + const remainingSeats = this.userOrg.seats - userCount; + + const seatsRemaining = this.i18nService.t( + "seatsRemaining", + remainingSeats.toString(), + this.userOrg.seats.toString(), + ); + + this.resellerSeatsRemainingMessage = seatsRemaining; + } } ngOnDestroy() { @@ -117,7 +144,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy async load() { this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); - this.userOrg = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const isIndependentOrganizationOwner = !this.userOrg.hasProvider && this.userOrg.isOwner; const isResoldOrganizationOwner = this.userOrg.hasReseller && this.userOrg.isOwner; @@ -415,36 +447,19 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy adjustStorage = (add: boolean) => { return async () => { - const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); - - if (deprecateStripeSourcesAPI) { - const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { - data: { - price: this.storageGbPrice, - cadence: this.billingInterval, - type: add ? "Add" : "Remove", - organizationId: this.organizationId, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustStorageDialogV2ResultType.Submitted) { - await this.load(); - } - } else { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: this.storageGbPrice, - add: add, - organizationId: this.organizationId, - interval: this.billingInterval, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); - } + const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { + data: { + price: this.storageGbPrice, + cadence: this.billingInterval, + type: add ? "Add" : "Remove", + organizationId: this.organizationId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustStorageDialogResultType.Submitted) { + await this.load(); } }; }; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index ef68de395267..e6854a5216b4 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -7,10 +7,15 @@ import { concatMap, firstValueFrom, 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationConnectionType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/models/response/organization-connection.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { BillingSyncConfigApi } from "@bitwarden/common/billing/models/api/billing-sync-config.api"; import { SelfHostedOrganizationSubscriptionView } from "@bitwarden/common/billing/models/view/self-hosted-organization-subscription.view"; @@ -80,6 +85,7 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest private messagingService: MessagingService, private apiService: ApiService, private organizationService: OrganizationService, + private accountService: AccountService, private route: ActivatedRoute, private organizationApiService: OrganizationApiServiceAbstraction, private platformUtilsService: PlatformUtilsService, @@ -115,7 +121,12 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest return; } this.loading = true; - this.userOrg = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); this.showAutomaticSyncAndManualUpload = this.userOrg.productTierType == ProductTierType.Families ? false : true; if (this.userOrg.canViewSubscription) { 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 index 270ba54f70da..a8b2c7a46f16 100644 --- 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 @@ -4,11 +4,15 @@ import { Location } from "@angular/common"; import { Component, OnDestroy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; -import { from, lastValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, from, lastValueFrom, map, switchMap } from "rxjs"; 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 { + getOrganizationById, + 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 { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; @@ -19,16 +23,16 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; -import { FreeTrial } from "../../../core/types/free-trial"; import { TrialFlowService } from "../../services/trial-flow.service"; import { AddCreditDialogResult, openAddCreditDialog, } from "../../shared/add-credit-dialog.component"; import { - AdjustPaymentDialogV2Component, - AdjustPaymentDialogV2ResultType, -} from "../../shared/adjust-payment-dialog/adjust-payment-dialog-v2.component"; + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, +} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component"; +import { FreeTrial } from "../../types/free-trial"; @Component({ templateUrl: "./organization-payment-method.component.html", @@ -60,6 +64,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { private location: Location, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, + private accountService: AccountService, protected syncService: SyncService, ) { this.activatedRoute.params @@ -120,7 +125,14 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); - const organizationPromise = this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ organizationSubscriptionPromise, @@ -147,7 +159,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }; protected updatePaymentMethod = async (): Promise => { - const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, @@ -157,13 +169,13 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogV2ResultType.Submitted) { + if (result === AdjustPaymentDialogResultType.Submitted) { await this.load(); } }; changePayment = async () => { - const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, @@ -171,7 +183,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }, }); const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogV2ResultType.Submitted) { + if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); 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 fc7a188f967f..c10c9abc9b62 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 @@ -2,11 +2,16 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + InternalOrganizationServiceAbstraction, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; 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"; @@ -107,6 +112,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest private platformUtilsService: PlatformUtilsService, private toastService: ToastService, private internalOrganizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -165,14 +171,19 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest request, ); - const organization = await this.internalOrganizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.internalOrganizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const organizationData = new OrganizationData(response, { isMember: organization.isMember, isProviderUser: organization.isProviderUser, }); - await this.internalOrganizationService.upsert(organizationData); + await this.internalOrganizationService.upsert(organizationData, userId); this.toastService.showToast({ variant: "success", 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 7ad0895809cf..617b68abb371 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 @@ -2,12 +2,15 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request"; import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; @@ -37,6 +40,7 @@ export class SecretsManagerSubscribeStandaloneComponent { private organizationApiService: OrganizationApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, private toastService: ToastService, + private accountService: AccountService, ) {} submit = async () => { @@ -56,7 +60,8 @@ export class SecretsManagerSubscribeStandaloneComponent { isMember: this.organization.isMember, isProviderUser: this.organization.isProviderUser, }); - await this.organizationService.upsert(organizationData); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + await this.organizationService.upsert(organizationData, userId); /* Because subscribing to Secrets Manager automatically provides access to Secrets Manager for the diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index cc53e0a32bc9..10d7a9c65908 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -1,11 +1,12 @@ import { Injectable } from "@angular/core"; -import { combineLatest, filter, from, map, Observable, of, switchMap } from "rxjs"; +import { combineLatest, filter, map, Observable, of, switchMap } 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; interface EnterpriseOrgStatus { @@ -25,23 +26,38 @@ export class FreeFamiliesPolicyService { constructor( private policyService: PolicyService, private organizationService: OrganizationService, + private accountService: AccountService, private configService: ConfigService, ) {} + canManageSponsorships$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account?.id) { + return this.organizationService.canManageSponsorships$(account?.id); + } else { + return of(); + } + }), + ); + + organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account?.id) { + return this.organizationService.organizations$(account?.id); + } else { + return of(); + } + }), + ); + get showFreeFamilies$(): Observable { - return this.isFreeFamilyFlagEnabled$.pipe( - switchMap((isFreeFamilyFlagEnabled) => - isFreeFamilyFlagEnabled - ? this.getFreeFamiliesVisibility$() - : this.organizationService.canManageSponsorships$, - ), - ); + return this.getFreeFamiliesVisibility$(); } private getFreeFamiliesVisibility$(): Observable { return combineLatest([ this.checkEnterpriseOrganizationsAndFetchPolicy(), - this.organizationService.canManageSponsorships$, + this.canManageSponsorships$, ]).pipe( map(([orgStatus, canManageSponsorships]) => this.shouldShowFreeFamilyLink(orgStatus, canManageSponsorships), @@ -61,7 +77,7 @@ export class FreeFamiliesPolicyService { } checkEnterpriseOrganizationsAndFetchPolicy(): Observable { - return this.organizationService.organizations$.pipe( + return this.organizations$.pipe( filter((organizations) => Array.isArray(organizations) && organizations.length > 0), switchMap((organizations) => this.fetchEnterpriseOrganizationPolicy(organizations)), ); @@ -90,7 +106,11 @@ export class FreeFamiliesPolicyService { }); } - return this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy).pipe( + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ), map((policies) => ({ isFreeFamilyPolicyEnabled: policies.some( (policy) => policy.organizationId === organizationId && policy.enabled, @@ -118,8 +138,4 @@ export class FreeFamiliesPolicyService { const enterpriseOrganizations = organizations.filter((org) => org.canManageSponsorships); return enterpriseOrganizations.length === 1 ? enterpriseOrganizations[0].id : null; } - - private get isFreeFamilyFlagEnabled$(): Observable { - return from(this.configService.getFeatureFlag(FeatureFlag.DisableFreeFamiliesSponsorship)); - } } diff --git a/apps/web/src/app/billing/services/stripe.service.ts b/apps/web/src/app/billing/services/stripe.service.ts index 61bc0b6cdd27..caf433347952 100644 --- a/apps/web/src/app/billing/services/stripe.service.ts +++ b/apps/web/src/app/billing/services/stripe.service.ts @@ -3,8 +3,6 @@ import { Injectable } from "@angular/core"; import { BankAccount } from "@bitwarden/common/billing/models/domain"; -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 { BillingServicesModule } from "./billing-services.module"; @@ -19,10 +17,7 @@ export class StripeService { cardCvc: string; }; - constructor( - private logService: LogService, - private configService: ConfigService, - ) {} + constructor(private logService: LogService) {} /** * Loads [Stripe JS]{@link https://docs.stripe.com/js} in the element of the current page and mounts @@ -43,19 +38,10 @@ export class StripeService { const window$ = window as any; this.stripe = window$.Stripe(process.env.STRIPE_KEY); this.elements = this.stripe.elements(); - const isExtensionRefresh = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); setTimeout(() => { - this.elements.create( - "cardNumber", - this.getElementOptions("cardNumber", isExtensionRefresh), - ); - this.elements.create( - "cardExpiry", - this.getElementOptions("cardExpiry", isExtensionRefresh), - ); - this.elements.create("cardCvc", this.getElementOptions("cardCvc", isExtensionRefresh)); + this.elements.create("cardNumber", this.getElementOptions("cardNumber")); + this.elements.create("cardExpiry", this.getElementOptions("cardExpiry")); + this.elements.create("cardCvc", this.getElementOptions("cardCvc")); if (autoMount) { this.mountElements(); } @@ -150,10 +136,7 @@ export class StripeService { }, 500); } - private getElementOptions( - element: "cardNumber" | "cardExpiry" | "cardCvc", - isExtensionRefresh: boolean, - ): any { + private getElementOptions(element: "cardNumber" | "cardExpiry" | "cardCvc"): any { const options: any = { style: { base: { @@ -178,15 +161,12 @@ export class StripeService { }, }; - // Unique settings that should only be applied when the extension refresh flag is active - if (isExtensionRefresh) { - options.style.base.fontWeight = "500"; - options.classes.base = "v2"; + options.style.base.fontWeight = "500"; + options.classes.base = "v2"; - // Remove the placeholder for number and CVC fields - if (["cardNumber", "cardCvc"].includes(element)) { - options.placeholder = ""; - } + // Remove the placeholder for number and CVC fields + if (["cardNumber", "cardCvc"].includes(element)) { + options.placeholder = ""; } const style = getComputedStyle(document.documentElement); diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts index a3a4ba6bba1e..eb08e5bd7ade 100644 --- a/apps/web/src/app/billing/services/trial-flow.service.ts +++ b/apps/web/src/app/billing/services/trial-flow.service.ts @@ -16,11 +16,11 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService } from "@bitwarden/components"; -import { FreeTrial } from "../../core/types/free-trial"; import { ChangePlanDialogResultType, openChangePlanDialog, } from "../organizations/change-plan-dialog.component"; +import { FreeTrial } from "../types/free-trial"; @Injectable({ providedIn: "root" }) export class TrialFlowService { diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index 5e26e80a30a5..c35fd3a2e611 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -19,9 +19,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlanSponsorshipType } 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -40,7 +39,6 @@ interface RequestSponsorshipForm { }) export class SponsoredFamiliesComponent implements OnInit, OnDestroy { loading = false; - isFreeFamilyFlagEnabled: boolean; availableSponsorshipOrgs$: Observable; activeSponsorshipOrgs$: Observable; @@ -63,7 +61,6 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { private formBuilder: FormBuilder, private accountService: AccountService, private toastService: ToastService, - private configService: ConfigService, private policyService: PolicyService, private freeFamiliesPolicyService: FreeFamiliesPolicyService, private router: Router, @@ -86,36 +83,28 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { } async ngOnInit() { - this.isFreeFamilyFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.DisableFreeFamiliesSponsorship, + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + await this.preventAccessToFreeFamiliesPage(); + + this.availableSponsorshipOrgs$ = combineLatest([ + this.organizationService.organizations$(userId), + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ]).pipe( + map(([organizations, policies]) => + organizations + .filter((org) => org.familySponsorshipAvailable) + .map((org) => ({ + organization: org, + isPolicyEnabled: policies.some( + (policy) => policy.organizationId === org.id && policy.enabled, + ), + })) + .filter(({ isPolicyEnabled }) => !isPolicyEnabled) + .map(({ organization }) => organization), + ), ); - if (this.isFreeFamilyFlagEnabled) { - await this.preventAccessToFreeFamiliesPage(); - - this.availableSponsorshipOrgs$ = combineLatest([ - this.organizationService.organizations$, - this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy), - ]).pipe( - map(([organizations, policies]) => - organizations - .filter((org) => org.familySponsorshipAvailable) - .map((org) => ({ - organization: org, - isPolicyEnabled: policies.some( - (policy) => policy.organizationId === org.id && policy.enabled, - ), - })) - .filter(({ isPolicyEnabled }) => !isPolicyEnabled) - .map(({ organization }) => organization), - ), - ); - } else { - this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)), - ); - } - this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { if (orgs.length === 1) { this.sponsorshipForm.patchValue({ @@ -126,9 +115,9 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); - this.activeSponsorshipOrgs$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null)), - ); + this.activeSponsorshipOrgs$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null))); this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index b40902112c8c..e613b8629222 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -2,13 +2,14 @@ // @ts-strict-ignore import { formatDate } from "@angular/common"; import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -27,7 +28,6 @@ export class SponsoringOrgRowComponent implements OnInit { statusMessage = "loading"; statusClass: "tw-text-success" | "tw-text-danger" = "tw-text-success"; isFreeFamilyPolicyEnabled$: Observable; - isFreeFamilyFlagEnabled: boolean; private locale = ""; constructor( @@ -38,6 +38,7 @@ export class SponsoringOrgRowComponent implements OnInit { private toastService: ToastService, private configService: ConfigService, private policyService: PolicyService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -49,23 +50,20 @@ export class SponsoringOrgRowComponent implements OnInit { this.sponsoringOrg.familySponsorshipValidUntil, this.sponsoringOrg.familySponsorshipLastSyncDate, ); - this.isFreeFamilyFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.DisableFreeFamiliesSponsorship, - ); - if (this.isFreeFamilyFlagEnabled) { - this.isFreeFamilyPolicyEnabled$ = this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy) - .pipe( - map( - (policies) => - Array.isArray(policies) && - policies.some( - (policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled, - ), + this.isFreeFamilyPolicyEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ), + map( + (policies) => + Array.isArray(policies) && + policies.some( + (policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled, ), - ); - } + ), + ); } async revokeSponsorship() { diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index 71afde81ee37..7860d4566852 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -6,7 +6,10 @@ import { FormControl, FormGroup, Validators } from "@angular/forms"; import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; @@ -77,7 +80,14 @@ export class AddCreditDialogComponent implements OnInit { this.creditAmount = "20.00"; } this.ppButtonCustomField = "organization_id:" + this.organizationId; - const org = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); if (org != null) { this.subject = org.name; this.name = org.name; diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html deleted file mode 100644 index bb06f87ca03c..000000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts deleted file mode 100644 index 0a72b8302bcf..000000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts +++ /dev/null @@ -1,177 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core"; - -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { PaymentV2Component } from "../payment/payment-v2.component"; - -export interface AdjustPaymentDialogV2Params { - initialPaymentMethod?: PaymentMethodType; - organizationId?: string; - productTier?: ProductTierType; -} - -export enum AdjustPaymentDialogV2ResultType { - Closed = "closed", - Submitted = "submitted", -} - -@Component({ - templateUrl: "./adjust-payment-dialog-v2.component.html", -}) -export class AdjustPaymentDialogV2Component implements OnInit { - @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component; - @ViewChild(forwardRef(() => ManageTaxInformationComponent)) - taxInfoComponent: ManageTaxInformationComponent; - - protected readonly PaymentMethodType = PaymentMethodType; - protected readonly ResultType = AdjustPaymentDialogV2ResultType; - - protected dialogHeader: string; - protected initialPaymentMethod: PaymentMethodType; - protected organizationId?: string; - protected productTier?: ProductTierType; - - protected taxInformation: TaxInformation; - - constructor( - private apiService: ApiService, - private billingApiService: BillingApiServiceAbstraction, - private organizationApiService: OrganizationApiServiceAbstraction, - @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogV2Params, - private dialogRef: DialogRef, - private i18nService: I18nService, - private toastService: ToastService, - ) { - const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; - this.dialogHeader = this.i18nService.t(key); - this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; - this.organizationId = this.dialogParams.organizationId; - this.productTier = this.dialogParams.productTier; - } - - ngOnInit(): void { - if (this.organizationId) { - this.organizationApiService - .getTaxInfo(this.organizationId) - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }); - } else { - this.apiService - .getTaxInfo() - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }); - } - } - - taxInformationChanged(event: TaxInformation) { - this.taxInformation = event; - if (event.country === "US") { - this.paymentComponent.showBankAccount = !!this.organizationId; - } else { - this.paymentComponent.showBankAccount = false; - if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - } - - submit = async (): Promise => { - if (!this.taxInfoComponent.validate()) { - return; - } - - try { - if (!this.organizationId) { - await this.updatePremiumUserPaymentMethod(); - } else { - await this.updateOrganizationPaymentMethod(); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedPaymentMethod"), - }); - - this.dialogRef.close(AdjustPaymentDialogV2ResultType.Submitted); - } catch (error) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t(error.message) || error.message, - }); - } - }; - - private updateOrganizationPaymentMethod = async () => { - const paymentSource = await this.paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); - - await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); - }; - - protected get showTaxIdField(): boolean { - if (!this.organizationId) { - return false; - } - - switch (this.productTier) { - case ProductTierType.Free: - case ProductTierType.Families: - return false; - default: - return true; - } - } - - private updatePremiumUserPaymentMethod = async () => { - const { type, token } = await this.paymentComponent.tokenize(); - - const request = new PaymentRequest(); - request.paymentMethodType = type; - request.paymentToken = token; - request.country = this.taxInformation.country; - request.postalCode = this.taxInformation.postalCode; - request.taxId = this.taxInformation.taxId; - request.state = this.taxInformation.state; - request.line1 = this.taxInformation.line1; - request.line2 = this.taxInformation.line2; - request.city = this.taxInformation.city; - request.state = this.taxInformation.state; - await this.apiService.postAccountPayment(request); - }; - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open( - AdjustPaymentDialogV2Component, - dialogConfig, - ); -} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html index de6073143543..4f7990f11a3d 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html @@ -1,30 +1,29 @@ - - - - - - - - - - - - + + + + + + + + + + diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index bbae5099afa7..0fc49b2ddc1d 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -1,59 +1,66 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, OnInit, ViewChild } from "@angular/core"; -import { FormGroup } from "@angular/forms"; +import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; +import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PaymentComponent } from "../payment/payment.component"; -export interface AdjustPaymentDialogData { - organizationId: string; - currentType: PaymentMethodType; +export interface AdjustPaymentDialogParams { + initialPaymentMethod?: PaymentMethodType; + organizationId?: string; + productTier?: ProductTierType; } -export enum AdjustPaymentDialogResult { - Adjusted = "adjusted", - Cancelled = "cancelled", +export enum AdjustPaymentDialogResultType { + Closed = "closed", + Submitted = "submitted", } @Component({ - templateUrl: "adjust-payment-dialog.component.html", + templateUrl: "./adjust-payment-dialog.component.html", }) export class AdjustPaymentDialogComponent implements OnInit { - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent; + @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; + @ViewChild(forwardRef(() => ManageTaxInformationComponent)) + taxInfoComponent: ManageTaxInformationComponent; - organizationId: string; - currentType: PaymentMethodType; - paymentMethodType = PaymentMethodType; + protected readonly PaymentMethodType = PaymentMethodType; + protected readonly ResultType = AdjustPaymentDialogResultType; - protected DialogResult = AdjustPaymentDialogResult; - protected formGroup = new FormGroup({}); + protected dialogHeader: string; + protected initialPaymentMethod: PaymentMethodType; + protected organizationId?: string; + protected productTier?: ProductTierType; protected taxInformation: TaxInformation; constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData, private apiService: ApiService, - private i18nService: I18nService, + private billingApiService: BillingApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, private toastService: ToastService, ) { - this.organizationId = data.organizationId; - this.currentType = data.currentType; + const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; + this.dialogHeader = this.i18nService.t(key); + this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; + this.organizationId = this.dialogParams.organizationId; + this.productTier = this.dialogParams.productTier; } ngOnInit(): void { @@ -78,65 +85,92 @@ export class AdjustPaymentDialogComponent implements OnInit { } } - submit = async () => { - if (!this.taxInfoComponent?.validate()) { - return; - } - - const request = new PaymentRequest(); - const response = this.paymentComponent.createPaymentToken().then((result) => { - request.paymentToken = result[0]; - request.paymentMethodType = result[1]; - request.postalCode = this.taxInformation?.postalCode; - request.country = this.taxInformation?.country; - request.taxId = this.taxInformation?.taxId; - if (this.organizationId == null) { - return this.apiService.postAccountPayment(request); - } else { - request.taxId = this.taxInformation?.taxId; - request.state = this.taxInformation?.state; - request.line1 = this.taxInformation?.line1; - request.line2 = this.taxInformation?.line2; - request.city = this.taxInformation?.city; - request.state = this.taxInformation?.state; - return this.organizationApiService.updatePayment(this.organizationId, request); - } - }); - await response; - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedPaymentMethod"), - }); - this.dialogRef.close(AdjustPaymentDialogResult.Adjusted); - }; - taxInformationChanged(event: TaxInformation) { this.taxInformation = event; if (event.country === "US") { - this.paymentComponent.hideBank = !this.organizationId; + this.paymentComponent.showBankAccount = !!this.organizationId; } else { - this.paymentComponent.hideBank = true; - if (this.paymentComponent.method === PaymentMethodType.BankAccount) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); + this.paymentComponent.showBankAccount = false; + if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { + this.paymentComponent.select(PaymentMethodType.Card); } } } + submit = async (): Promise => { + if (!this.taxInfoComponent.validate()) { + this.taxInfoComponent.markAllAsTouched(); + return; + } + + try { + if (!this.organizationId) { + await this.updatePremiumUserPaymentMethod(); + } else { + await this.updateOrganizationPaymentMethod(); + } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedPaymentMethod"), + }); + + this.dialogRef.close(AdjustPaymentDialogResultType.Submitted); + } catch (error) { + const msg = typeof error == "object" ? error.message : error; + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t(msg) || msg, + }); + } + }; + + private updateOrganizationPaymentMethod = async () => { + const paymentSource = await this.paymentComponent.tokenize(); + + const request = new UpdatePaymentMethodRequest(); + request.paymentSource = paymentSource; + request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); + + await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); + }; + protected get showTaxIdField(): boolean { - return !!this.organizationId; + if (!this.organizationId) { + return false; + } + + switch (this.productTier) { + case ProductTierType.Free: + case ProductTierType.Families: + return false; + default: + return true; + } } -} -/** - * Strongly typed helper to open a AdjustPaymentDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAdjustPaymentDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AdjustPaymentDialogComponent, config); + private updatePremiumUserPaymentMethod = async () => { + const { type, token } = await this.paymentComponent.tokenize(); + + const request = new PaymentRequest(); + request.paymentMethodType = type; + request.paymentToken = token; + request.country = this.taxInformation.country; + request.postalCode = this.taxInformation.postalCode; + request.taxId = this.taxInformation.taxId; + request.state = this.taxInformation.state; + request.line1 = this.taxInformation.line1; + request.line2 = this.taxInformation.line2; + request.city = this.taxInformation.city; + request.state = this.taxInformation.state; + await this.apiService.postAccountPayment(request); + }; + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open(AdjustPaymentDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html deleted file mode 100644 index 7b74379acb6a..000000000000 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html +++ /dev/null @@ -1,34 +0,0 @@ -
- - -

{{ body }}

-
- - {{ storageFieldLabel }} - - - - {{ "total" | i18n }} - {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = - {{ this.price * this.formGroup.value.storage | currency: "$" }} / - {{ this.cadence | i18n }} - - -
-
- - - - -
-
diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts deleted file mode 100644 index ba7619729bfc..000000000000 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts +++ /dev/null @@ -1,106 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -export interface AdjustStorageDialogV2Params { - price: number; - cadence: "month" | "year"; - type: "Add" | "Remove"; - organizationId?: string; -} - -export enum AdjustStorageDialogV2ResultType { - Submitted = "submitted", - Closed = "closed", -} - -@Component({ - templateUrl: "./adjust-storage-dialog-v2.component.html", -}) -export class AdjustStorageDialogV2Component { - protected formGroup = new FormGroup({ - storage: new FormControl(0, [ - Validators.required, - Validators.min(0), - Validators.max(99), - ]), - }); - - protected organizationId?: string; - protected price: number; - protected cadence: "month" | "year"; - - protected title: string; - protected body: string; - protected storageFieldLabel: string; - - protected ResultType = AdjustStorageDialogV2ResultType; - - constructor( - private apiService: ApiService, - @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params, - private dialogRef: DialogRef, - private i18nService: I18nService, - private organizationApiService: OrganizationApiServiceAbstraction, - private toastService: ToastService, - ) { - this.price = this.dialogParams.price; - this.cadence = this.dialogParams.cadence; - this.organizationId = this.dialogParams.organizationId; - switch (this.dialogParams.type) { - case "Add": - this.title = this.i18nService.t("addStorage"); - this.body = this.i18nService.t("storageAddNote"); - this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); - break; - case "Remove": - this.title = this.i18nService.t("removeStorage"); - this.body = this.i18nService.t("storageRemoveNote"); - this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); - break; - } - } - - submit = async () => { - const request = new StorageRequest(); - switch (this.dialogParams.type) { - case "Add": - request.storageGbAdjustment = this.formGroup.value.storage; - break; - case "Remove": - request.storageGbAdjustment = this.formGroup.value.storage * -1; - break; - } - - if (this.organizationId) { - await this.organizationApiService.updateStorage(this.organizationId, request); - } else { - await this.apiService.postAccountStorage(request); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - }); - - this.dialogRef.close(this.ResultType.Submitted); - }; - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open( - AdjustStorageDialogV2Component, - dialogConfig, - ); -} diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html index a597a3ae5ea4..832356477c4d 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html @@ -1,17 +1,17 @@
- + -

{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}

+

{{ body }}

- {{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }} - - - {{ "total" | i18n }}: - {{ formGroup.get("storageAdjustment").value || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ - interval | i18n - }} + {{ storageFieldLabel }} + + + + {{ "total" | i18n }} + {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = + {{ this.price * this.formGroup.value.storage | currency: "$" }} / + {{ this.cadence | i18n }}
@@ -25,11 +25,10 @@ bitButton bitFormButton buttonType="secondary" - [bitDialogClose]="DialogResult.Cancelled" + [bitDialogClose]="ResultType.Closed" > {{ "cancel" | i18n }}
- diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index f69f9e3eaad7..4362e36f857c 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -1,132 +1,103 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, ViewChild } from "@angular/core"; +import { Component, Inject } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentResponse } from "@bitwarden/common/billing/models/response/payment.response"; import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; 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, ToastService } from "@bitwarden/components"; -import { PaymentComponent } from "../payment/payment.component"; - -export interface AdjustStorageDialogData { - storageGbPrice: number; - add: boolean; +export interface AdjustStorageDialogParams { + price: number; + cadence: "month" | "year"; + type: "Add" | "Remove"; organizationId?: string; - interval?: string; } -export enum AdjustStorageDialogResult { - Adjusted = "adjusted", - Cancelled = "cancelled", +export enum AdjustStorageDialogResultType { + Submitted = "submitted", + Closed = "closed", } @Component({ - templateUrl: "adjust-storage-dialog.component.html", + templateUrl: "./adjust-storage-dialog.component.html", }) export class AdjustStorageDialogComponent { - storageGbPrice: number; - add: boolean; - organizationId: string; - interval: string; - - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - - protected DialogResult = AdjustStorageDialogResult; protected formGroup = new FormGroup({ - storageAdjustment: new FormControl(0, [ + storage: new FormControl(0, [ Validators.required, Validators.min(0), Validators.max(99), ]), }); + protected organizationId?: string; + protected price: number; + protected cadence: "month" | "year"; + + protected title: string; + protected body: string; + protected storageFieldLabel: string; + + protected ResultType = AdjustStorageDialogResultType; + constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AdjustStorageDialogData, private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogParams, + private dialogRef: DialogRef, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private router: Router, - private activatedRoute: ActivatedRoute, - private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, private toastService: ToastService, ) { - this.storageGbPrice = data.storageGbPrice; - this.add = data.add; - this.organizationId = data.organizationId; - this.interval = data.interval || "year"; + this.price = this.dialogParams.price; + this.cadence = this.dialogParams.cadence; + this.organizationId = this.dialogParams.organizationId; + switch (this.dialogParams.type) { + case "Add": + this.title = this.i18nService.t("addStorage"); + this.body = this.i18nService.t("storageAddNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); + break; + case "Remove": + this.title = this.i18nService.t("removeStorage"); + this.body = this.i18nService.t("storageRemoveNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); + break; + } } submit = async () => { const request = new StorageRequest(); - request.storageGbAdjustment = this.formGroup.value.storageAdjustment; - if (!this.add) { - request.storageGbAdjustment *= -1; + switch (this.dialogParams.type) { + case "Add": + request.storageGbAdjustment = this.formGroup.value.storage; + break; + case "Remove": + request.storageGbAdjustment = this.formGroup.value.storage * -1; + break; } - let paymentFailed = false; - const action = async () => { - let response: Promise; - if (this.organizationId == null) { - response = this.apiService.postAccountStorage(request); - } else { - response = this.organizationApiService.updateStorage(this.organizationId, request); - } - const result = await response; - if (result != null && result.paymentIntentClientSecret != null) { - try { - await this.paymentComponent.handleStripeCardPayment( - result.paymentIntentClientSecret, - null, - ); - } catch { - paymentFailed = true; - } - } - }; - await action(); - this.dialogRef.close(AdjustStorageDialogResult.Adjusted); - if (paymentFailed) { - this.toastService.showToast({ - variant: "warning", - title: null, - message: this.i18nService.t("couldNotChargeCardPayInvoice"), - 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(["../billing"], { relativeTo: this.activatedRoute }); + if (this.organizationId) { + await this.organizationApiService.updateStorage(this.organizationId, request); } else { - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - }); + await this.apiService.postAccountStorage(request); } - }; - get adjustedStorageTotal(): number { - return this.storageGbPrice * this.formGroup.value.storageAdjustment; - } -} + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + }); + + this.dialogRef.close(this.ResultType.Submitted); + }; -/** - * Strongly typed helper to open an AdjustStorageDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAdjustStorageDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AdjustStorageDialogComponent, config); + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open(AdjustStorageDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index b9c235943ad1..9a69755b2097 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -6,13 +6,10 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { AddCreditDialogComponent } from "./add-credit-dialog.component"; -import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component"; import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component"; -import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; -import { PaymentV2Component } from "./payment/payment-v2.component"; import { PaymentComponent } from "./payment/payment.component"; import { PaymentMethodComponent } from "./payment-method.component"; import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component"; @@ -26,40 +23,35 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac @NgModule({ imports: [ SharedModule, - PaymentComponent, TaxInfoComponent, HeaderModule, BannerModule, - PaymentV2Component, + PaymentComponent, VerifyBankAccountComponent, ], declarations: [ AddCreditDialogComponent, - AdjustPaymentDialogComponent, - AdjustStorageDialogComponent, BillingHistoryComponent, PaymentMethodComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, - AdjustPaymentDialogV2Component, - AdjustStorageDialogV2Component, + AdjustPaymentDialogComponent, + AdjustStorageDialogComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], exports: [ SharedModule, - PaymentComponent, TaxInfoComponent, - AdjustStorageDialogComponent, BillingHistoryComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, VerifyBankAccountComponent, - PaymentV2Component, + PaymentComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], diff --git a/apps/web/src/app/billing/shared/index.ts b/apps/web/src/app/billing/shared/index.ts index 69a4b93bec80..54ab5bc0a2a5 100644 --- a/apps/web/src/app/billing/shared/index.ts +++ b/apps/web/src/app/billing/shared/index.ts @@ -1,5 +1,4 @@ export * from "./billing-shared.module"; export * from "./payment-method.component"; -export * from "./payment/payment.component"; export * from "./sm-subscribe.component"; export * from "./tax-info.component"; diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 149b4adf5206..dc031ade42f4 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -4,12 +4,16 @@ import { Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { lastValueFrom } from "rxjs"; +import { firstValueFrom, lastValueFrom, 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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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 { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -20,13 +24,13 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; -import { FreeTrial } from "../../core/types/free-trial"; import { TrialFlowService } from "../services/trial-flow.service"; +import { FreeTrial } from "../types/free-trial"; import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { - AdjustPaymentDialogResult, - openAdjustPaymentDialog, + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, } from "./adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ @@ -73,6 +77,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { private toastService: ToastService, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, + private accountService: AccountService, protected syncService: SyncService, ) { const state = this.router.getCurrentNavigation()?.extras?.state; @@ -117,7 +122,14 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); - const organizationPromise = this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); [this.billing, this.org, this.organization] = await Promise.all([ billingPromise, @@ -158,14 +170,16 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { }; changePayment = async () => { - const dialogRef = openAdjustPaymentDialog(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { organizationId: this.organizationId, - currentType: this.paymentSource !== null ? this.paymentSource.type : null, + initialPaymentMethod: this.paymentSource !== null ? this.paymentSource.type : null, }, }); + const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogResult.Adjusted) { + + if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html deleted file mode 100644 index a2e1c92a0500..000000000000 --- a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - -
- - - ({{ "required" | i18n }}) - -
-
- - - - diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts deleted file mode 100644 index f4d0f0977662..000000000000 --- a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { booleanAttribute, Component, Input, OnInit } from "@angular/core"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { FormFieldModule } from "@bitwarden/components"; - -import { SharedModule } from "../../../shared"; - -/** - * Label that should be used for elements loaded via Stripe API. - * - * Applies the same label styles from CL form-field component when - * the `ExtensionRefresh` flag is set. - */ -@Component({ - selector: "app-payment-label-v2", - templateUrl: "./payment-label-v2.component.html", - standalone: true, - imports: [FormFieldModule, SharedModule], -}) -export class PaymentLabelV2 implements OnInit { - /** `id` of the associated input */ - @Input({ required: true }) for: string; - /** Displays required text on the label */ - @Input({ transform: booleanAttribute }) required = false; - - protected extensionRefreshFlag = false; - - constructor(private configService: ConfigService) {} - - async ngOnInit(): Promise { - this.extensionRefreshFlag = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); - } -} diff --git a/apps/web/src/app/billing/shared/payment/payment-label.component.html b/apps/web/src/app/billing/shared/payment/payment-label.component.html new file mode 100644 index 000000000000..a931b0524e3a --- /dev/null +++ b/apps/web/src/app/billing/shared/payment/payment-label.component.html @@ -0,0 +1,13 @@ + + + + +
+ + + ({{ "required" | i18n }}) + +
diff --git a/apps/web/src/app/billing/shared/payment/payment-label.component.ts b/apps/web/src/app/billing/shared/payment/payment-label.component.ts new file mode 100644 index 000000000000..80a4087456d7 --- /dev/null +++ b/apps/web/src/app/billing/shared/payment/payment-label.component.ts @@ -0,0 +1,27 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { booleanAttribute, Component, Input } from "@angular/core"; + +import { FormFieldModule } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared"; + +/** + * Label that should be used for elements loaded via Stripe API. + * + * Applies the same label styles from CL form-field component + */ +@Component({ + selector: "app-payment-label", + templateUrl: "./payment-label.component.html", + standalone: true, + imports: [FormFieldModule, SharedModule], +}) +export class PaymentLabelComponent { + /** `id` of the associated input */ + @Input({ required: true }) for: string; + /** Displays required text on the label */ + @Input({ transform: booleanAttribute }) required = false; + + constructor() {} +} diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-v2.component.html deleted file mode 100644 index 9804e6bc86ff..000000000000 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.html +++ /dev/null @@ -1,152 +0,0 @@ -
-
- - - - - {{ "creditCard" | i18n }} - - - - - - {{ "bankAccount" | i18n }} - - - - - - {{ "payPal" | i18n }} - - - - - - {{ "accountCredit" | i18n }} - - - -
- - -
-
- - {{ "number" | i18n }} - -
-
-
- Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay -
-
- - {{ "expiration" | i18n }} - -
-
-
- - {{ "securityCodeSlashCVV" | i18n }} - - - - -
-
-
-
- - - - {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} - -
- - {{ "routingNumber" | i18n }} - - - - {{ "accountNumber" | i18n }} - - - - {{ "accountHolderName" | i18n }} - - - - {{ "bankAccountType" | i18n }} - - - - - - -
-
- - -
-
- {{ "paypalClickSubmit" | i18n }} -
-
- - - - {{ "makeSureEnoughCredit" | i18n }} - - - -
diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts deleted file mode 100644 index 10cf7ccb702c..000000000000 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts +++ /dev/null @@ -1,193 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject } from "rxjs"; -import { takeUntil } from "rxjs/operators"; - -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; - -import { SharedModule } from "../../../shared"; -import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; - -import { PaymentLabelV2 } from "./payment-label-v2.component"; - -/** - * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, - * optionally, submit it using the {@link onSubmit} function if it is provided. - * - * This component is meant to replace the existing {@link PaymentComponent} which is using the deprecated Stripe Sources API. - */ -@Component({ - selector: "app-payment-v2", - templateUrl: "./payment-v2.component.html", - standalone: true, - imports: [BillingServicesModule, SharedModule, PaymentLabelV2], -}) -export class PaymentV2Component implements OnInit, OnDestroy { - /** Show account credit as a payment option. */ - @Input() showAccountCredit: boolean = true; - /** Show bank account as a payment option. */ - @Input() showBankAccount: boolean = true; - /** Show PayPal as a payment option. */ - @Input() showPayPal: boolean = true; - - /** The payment method selected by default when the component renders. */ - @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; - /** If provided, will be invoked with the tokenized payment source during form submission. */ - @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - - @Output() submitted = new EventEmitter(); - - private destroy$ = new Subject(); - - protected formGroup = new FormGroup({ - paymentMethod: new FormControl(null), - bankInformation: new FormGroup({ - routingNumber: new FormControl("", [Validators.required]), - accountNumber: new FormControl("", [Validators.required]), - accountHolderName: new FormControl("", [Validators.required]), - accountHolderType: new FormControl("", [Validators.required]), - }), - }); - - protected PaymentMethodType = PaymentMethodType; - - constructor( - private billingApiService: BillingApiServiceAbstraction, - private braintreeService: BraintreeService, - private stripeService: StripeService, - ) {} - - ngOnInit(): void { - this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); - - this.stripeService.loadStripe( - { - cardNumber: "#stripe-card-number", - cardExpiry: "#stripe-card-expiry", - cardCvc: "#stripe-card-cvc", - }, - this.initialPaymentMethod === PaymentMethodType.Card, - ); - - if (this.showPayPal) { - this.braintreeService.loadBraintree( - "#braintree-container", - this.initialPaymentMethod === PaymentMethodType.PayPal, - ); - } - - this.formGroup - .get("paymentMethod") - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((type) => { - this.onPaymentMethodChange(type); - }); - } - - /** Programmatically select the provided payment method. */ - select = (paymentMethod: PaymentMethodType) => { - this.formGroup.get("paymentMethod").patchValue(paymentMethod); - }; - - protected submit = async () => { - const { type, token } = await this.tokenize(); - await this.onSubmit?.({ type, token }); - this.submitted.emit(type); - }; - - /** - * Tokenize the payment method information entered by the user against one of our payment providers. - * - * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} - * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} - * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} - * */ - async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { - const type = this.selected; - - if (this.usingStripe) { - const clientSecret = await this.billingApiService.createSetupIntent(type); - - if (this.usingBankAccount) { - const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { - accountHolderName: this.formGroup.value.bankInformation.accountHolderName, - routingNumber: this.formGroup.value.bankInformation.routingNumber, - accountNumber: this.formGroup.value.bankInformation.accountNumber, - accountHolderType: this.formGroup.value.bankInformation.accountHolderType, - }); - return { - type, - token, - }; - } - - if (this.usingCard) { - const token = await this.stripeService.setupCardPaymentMethod(clientSecret); - return { - type, - token, - }; - } - } - - if (this.usingPayPal) { - const token = await this.braintreeService.requestPaymentMethod(); - return { - type, - token, - }; - } - - return null; - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - this.stripeService.unloadStripe(); - if (this.showPayPal) { - this.braintreeService.unloadBraintree(); - } - } - - private onPaymentMethodChange(type: PaymentMethodType): void { - switch (type) { - case PaymentMethodType.Card: { - this.stripeService.mountElements(); - break; - } - case PaymentMethodType.PayPal: { - this.braintreeService.createDropin(); - break; - } - } - } - - get selected(): PaymentMethodType { - return this.formGroup.value.paymentMethod; - } - - protected get usingAccountCredit(): boolean { - return this.selected === PaymentMethodType.Credit; - } - - protected get usingBankAccount(): boolean { - return this.selected === PaymentMethodType.BankAccount; - } - - protected get usingCard(): boolean { - return this.selected === PaymentMethodType.Card; - } - - protected get usingPayPal(): boolean { - return this.selected === PaymentMethodType.PayPal; - } - - private get usingStripe(): boolean { - return this.usingBankAccount || this.usingCard; - } -} diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html index d4853713579f..af261155171e 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.html +++ b/apps/web/src/app/billing/shared/payment/payment.component.html @@ -1,96 +1,125 @@ -
-
- - +
+
+ + - {{ "creditCard" | i18n }} + {{ "creditCard" | i18n }} + - + - {{ "bankAccount" | i18n }} + {{ "bankAccount" | i18n }} + - - PayPal + + + + {{ "payPal" | i18n }} + - + - {{ "accountCredit" | i18n }} + {{ "accountCredit" | i18n }} +
- -
-
- {{ - "number" | i18n - }} -
+ + +
+
+ + {{ "number" | i18n }} + +
-
+
Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay
-
- {{ - "expiration" | i18n - }} -
+
+ + {{ "expiration" | i18n }} + +
-
- +
+ {{ "securityCodeSlashCVV" | i18n }} - -
+
+
- + + - {{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} + {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} -
- +
+ {{ "routingNumber" | i18n }} - + - + {{ "accountNumber" | i18n }} - + - + {{ "accountHolderName" | i18n }} - - + {{ "bankAccountType" | i18n }} - +
- + +
-
+
{{ "paypalClickSubmit" | i18n }}
- - + + + {{ "makeSureEnoughCredit" | i18n }} - + -
+ + diff --git a/apps/web/src/app/billing/shared/payment/payment.component.ts b/apps/web/src/app/billing/shared/payment/payment.component.ts index e067a5ee490d..c11dfddb6ccd 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment.component.ts @@ -1,330 +1,203 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; -import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; import { SharedModule } from "../../../shared"; +import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; -import { PaymentLabelV2 } from "./payment-label-v2.component"; +import { PaymentLabelComponent } from "./payment-label.component"; +/** + * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, + * optionally, submit it using the {@link onSubmit} function if it is provided. + */ @Component({ selector: "app-payment", - templateUrl: "payment.component.html", + templateUrl: "./payment.component.html", standalone: true, - imports: [SharedModule, PaymentLabelV2], + imports: [BillingServicesModule, SharedModule, PaymentLabelComponent], }) export class PaymentComponent implements OnInit, OnDestroy { - @Input() showMethods = true; - @Input() showOptions = true; - @Input() hideBank = false; - @Input() hidePaypal = false; - @Input() hideCredit = false; - @Input() trialFlow = false; + /** Show account credit as a payment option. */ + @Input() showAccountCredit: boolean = true; + /** Show bank account as a payment option. */ + @Input() showBankAccount: boolean = true; + /** Show PayPal as a payment option. */ + @Input() showPayPal: boolean = true; - @Input() - set method(value: PaymentMethodType) { - this._method = value; - this.paymentForm?.controls.method.setValue(value, { emitEvent: false }); - } + /** The payment method selected by default when the component renders. */ + @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; + /** If provided, will be invoked with the tokenized payment source during form submission. */ + @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - get method(): PaymentMethodType { - return this._method; - } - private _method: PaymentMethodType = PaymentMethodType.Card; + @Output() submitted = new EventEmitter(); private destroy$ = new Subject(); - protected paymentForm = new FormGroup({ - method: new FormControl(this.method), - bank: new FormGroup({ - routing_number: new FormControl(null, [Validators.required]), - account_number: new FormControl(null, [Validators.required]), - account_holder_name: new FormControl(null, [Validators.required]), - account_holder_type: new FormControl("", [Validators.required]), - currency: new FormControl("USD"), - country: new FormControl("US"), + + protected formGroup = new FormGroup({ + paymentMethod: new FormControl(null), + bankInformation: new FormGroup({ + routingNumber: new FormControl("", [Validators.required]), + accountNumber: new FormControl("", [Validators.required]), + accountHolderName: new FormControl("", [Validators.required]), + accountHolderType: new FormControl("", [Validators.required]), }), }); - paymentMethodType = PaymentMethodType; - private btScript: HTMLScriptElement; - private btInstance: any = null; - private stripeScript: HTMLScriptElement; - private stripe: any = null; - private stripeElements: any = null; - private stripeCardNumberElement: any = null; - private stripeCardExpiryElement: any = null; - private stripeCardCvcElement: any = null; - private StripeElementStyle: any; - private StripeElementClasses: any; + protected PaymentMethodType = PaymentMethodType; constructor( - private apiService: ApiService, - private logService: LogService, - private themingService: AbstractThemingService, - private configService: ConfigService, - ) { - this.stripeScript = window.document.createElement("script"); - this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false"; - this.stripeScript.async = true; - this.stripeScript.onload = async () => { - this.stripe = (window as any).Stripe(process.env.STRIPE_KEY); - this.stripeElements = this.stripe.elements(); - await this.setStripeElement(); - }; - this.btScript = window.document.createElement("script"); - this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`; - this.btScript.async = true; - this.StripeElementStyle = { - base: { - color: null, - fontFamily: - '"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' + - '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', - fontSize: "16px", - fontSmoothing: "antialiased", - "::placeholder": { - color: null, - }, + private billingApiService: BillingApiServiceAbstraction, + private braintreeService: BraintreeService, + private stripeService: StripeService, + ) {} + + ngOnInit(): void { + this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); + + this.stripeService.loadStripe( + { + cardNumber: "#stripe-card-number", + cardExpiry: "#stripe-card-expiry", + cardCvc: "#stripe-card-cvc", }, - invalid: { - color: null, - }, - }; - this.StripeElementClasses = { - focus: "is-focused", - empty: "is-empty", - invalid: "is-invalid", - }; - } - async ngOnInit() { - if (!this.showOptions) { - this.hidePaypal = this.method !== PaymentMethodType.PayPal; - this.hideBank = this.method !== PaymentMethodType.BankAccount; - this.hideCredit = this.method !== PaymentMethodType.Credit; - } - this.subscribeToTheme(); - window.document.head.appendChild(this.stripeScript); - if (!this.hidePaypal) { - window.document.head.appendChild(this.btScript); + this.initialPaymentMethod === PaymentMethodType.Card, + ); + + if (this.showPayPal) { + this.braintreeService.loadBraintree( + "#braintree-container", + this.initialPaymentMethod === PaymentMethodType.PayPal, + ); } - this.paymentForm - .get("method") + + this.formGroup + .get("paymentMethod") .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((v) => { - this.method = v; - this.changeMethod(); + .subscribe((type) => { + this.onPaymentMethodChange(type); }); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - window.document.head.removeChild(this.stripeScript); - window.setTimeout(() => { - Array.from(window.document.querySelectorAll("iframe")).forEach((el) => { - if (el.src != null && el.src.indexOf("stripe") > -1) { - try { - window.document.body.removeChild(el); - } catch (e) { - this.logService.error(e); - } - } - }); - }, 500); - if (!this.hidePaypal) { - window.document.head.removeChild(this.btScript); - window.setTimeout(() => { - Array.from(window.document.head.querySelectorAll("script")).forEach((el) => { - if (el.src != null && el.src.indexOf("paypal") > -1) { - try { - window.document.head.removeChild(el); - } catch (e) { - this.logService.error(e); - } - } - }); - const btStylesheet = window.document.head.querySelector("#braintree-dropin-stylesheet"); - if (btStylesheet != null) { - try { - window.document.head.removeChild(btStylesheet); - } catch (e) { - this.logService.error(e); - } + /** Programmatically select the provided payment method. */ + select = (paymentMethod: PaymentMethodType) => { + this.formGroup.get("paymentMethod").patchValue(paymentMethod); + }; + + protected submit = async () => { + const { type, token } = await this.tokenize(); + await this.onSubmit?.({ type, token }); + this.submitted.emit(type); + }; + + /** + * Tokenize the payment method information entered by the user against one of our payment providers. + * + * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} + * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} + * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} + * */ + async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { + const type = this.selected; + + if (this.usingStripe) { + const clientSecret = await this.billingApiService.createSetupIntent(type); + + if (this.usingBankAccount) { + this.formGroup.markAllAsTouched(); + if (this.formGroup.valid) { + const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { + accountHolderName: this.formGroup.value.bankInformation.accountHolderName, + routingNumber: this.formGroup.value.bankInformation.routingNumber, + accountNumber: this.formGroup.value.bankInformation.accountNumber, + accountHolderType: this.formGroup.value.bankInformation.accountHolderType, + }); + return { + type, + token, + }; + } else { + throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again."; } - }, 500); + } + + if (this.usingCard) { + const token = await this.stripeService.setupCardPaymentMethod(clientSecret); + return { + type, + token, + }; + } } + + if (this.usingPayPal) { + const token = await this.braintreeService.requestPaymentMethod(); + return { + type, + token, + }; + } + + if (this.usingAccountCredit) { + return { + type: PaymentMethodType.Credit, + token: null, + }; + } + + return null; } - changeMethod() { - this.btInstance = null; - if (this.method === PaymentMethodType.PayPal) { - window.setTimeout(() => { - (window as any).braintree.dropin.create( - { - authorization: process.env.BRAINTREE_KEY, - container: "#bt-dropin-container", - paymentOptionPriority: ["paypal"], - paypal: { - flow: "vault", - buttonStyle: { - label: "pay", - size: "medium", - shape: "pill", - color: "blue", - tagline: "false", - }, - }, - }, - (createErr: any, instance: any) => { - if (createErr != null) { - // eslint-disable-next-line - console.error(createErr); - return; - } - this.btInstance = instance; - }, - ); - }, 250); - } else { - void this.setStripeElement(); + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.stripeService.unloadStripe(); + if (this.showPayPal) { + this.braintreeService.unloadBraintree(); } } - createPaymentToken(): Promise<[string, PaymentMethodType]> { - return new Promise((resolve, reject) => { - if (this.method === PaymentMethodType.Credit) { - resolve([null, this.method]); - } else if (this.method === PaymentMethodType.PayPal) { - this.btInstance - .requestPaymentMethod() - .then((payload: any) => { - resolve([payload.nonce, this.method]); - }) - .catch((err: any) => { - reject(err.message); - }); - } else if ( - this.method === PaymentMethodType.Card || - this.method === PaymentMethodType.BankAccount - ) { - if (this.method === PaymentMethodType.Card) { - // 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.apiService - .postSetupPayment() - .then((clientSecret) => - this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement), - ) - .then((result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.setupIntent && result.setupIntent.status === "succeeded") { - resolve([result.setupIntent.payment_method, this.method]); - } else { - reject(); - } - }); - } else { - this.stripe - .createToken("bank_account", this.paymentForm.get("bank").value) - .then((result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.token && result.token.id != null) { - resolve([result.token.id, this.method]); - } else { - reject(); - } - }); - } + private onPaymentMethodChange(type: PaymentMethodType): void { + switch (type) { + case PaymentMethodType.Card: { + this.stripeService.mountElements(); + break; + } + case PaymentMethodType.PayPal: { + this.braintreeService.createDropin(); + break; } - }); + } } - handleStripeCardPayment(clientSecret: string, successCallback: () => Promise): Promise { - return new Promise((resolve, reject) => { - if (this.showMethods && this.stripeCardNumberElement == null) { - reject(); - return; - } - const handleCardPayment = () => - this.showMethods - ? this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement) - : this.stripe.handleCardSetup(clientSecret); - return handleCardPayment().then(async (result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.paymentIntent && result.paymentIntent.status === "succeeded") { - if (successCallback != null) { - await successCallback(); - } - resolve(); - } else { - reject(); - } - }); - }); + get selected(): PaymentMethodType { + return this.formGroup.value.paymentMethod; } - private async setStripeElement() { - const extensionRefreshFlag = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); + protected get usingAccountCredit(): boolean { + return this.selected === PaymentMethodType.Credit; + } - // Apply unique styles for extension refresh - if (extensionRefreshFlag) { - this.StripeElementStyle.base.fontWeight = "500"; - this.StripeElementClasses.base = "v2"; - } + protected get usingBankAccount(): boolean { + return this.selected === PaymentMethodType.BankAccount; + } - window.setTimeout(() => { - if (this.showMethods && this.method === PaymentMethodType.Card) { - if (this.stripeCardNumberElement == null) { - this.stripeCardNumberElement = this.stripeElements.create("cardNumber", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - placeholder: "", - }); - } - if (this.stripeCardExpiryElement == null) { - this.stripeCardExpiryElement = this.stripeElements.create("cardExpiry", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - }); - } - if (this.stripeCardCvcElement == null) { - this.stripeCardCvcElement = this.stripeElements.create("cardCvc", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - placeholder: "", - }); - } - this.stripeCardNumberElement.mount("#stripe-card-number-element"); - this.stripeCardExpiryElement.mount("#stripe-card-expiry-element"); - this.stripeCardCvcElement.mount("#stripe-card-cvc-element"); - } - }, 50); + protected get usingCard(): boolean { + return this.selected === PaymentMethodType.Card; + } + + protected get usingPayPal(): boolean { + return this.selected === PaymentMethodType.PayPal; } - private subscribeToTheme() { - this.themingService.theme$.pipe(takeUntil(this.destroy$)).subscribe(() => { - const style = getComputedStyle(document.documentElement); - this.StripeElementStyle.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`; - this.StripeElementStyle.base["::placeholder"].color = `rgb(${style.getPropertyValue( - "--color-text-muted", - )})`; - this.StripeElementStyle.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`; - this.StripeElementStyle.invalid.borderColor = `rgb(${style.getPropertyValue( - "--color-danger-600", - )})`; - }); + private get usingStripe(): boolean { + return this.usingBankAccount || this.usingCard; } } diff --git a/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts b/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts index 41cc977d46fd..c8d5eac20999 100644 --- a/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts +++ b/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts @@ -7,7 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html rename to apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts similarity index 99% rename from apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts rename to apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts index 96c50a813194..873ceea2ada8 100644 --- a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts @@ -25,13 +25,13 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; +import { AcceptOrganizationInviteService } from "../../../auth/organization-invite/accept-organization.service"; import { OrganizationCreatedEvent, SubscriptionProduct, TrialOrganizationType, } from "../../../billing/accounts/trial-initiation/trial-billing-step.component"; import { RouterService } from "../../../core/router.service"; -import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service"; import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component"; export type InitiationPath = diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts rename to apps/web/src/app/billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts rename to apps/web/src/app/billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts diff --git a/apps/web/src/app/auth/trial-initiation/confirmation-details.component.html b/apps/web/src/app/billing/trial-initiation/confirmation-details.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/confirmation-details.component.html rename to apps/web/src/app/billing/trial-initiation/confirmation-details.component.html diff --git a/apps/web/src/app/auth/trial-initiation/confirmation-details.component.ts b/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/confirmation-details.component.ts rename to apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/abm-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/abm-enterprise-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/abm-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/abm-enterprise-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/abm-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/abm-teams-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/abm-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/abm-teams-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-enterprise-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-enterprise-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-individual-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-individual-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-individual-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-individual-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-teams-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-teams-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/default-content.component.html b/apps/web/src/app/billing/trial-initiation/content/default-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/default-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/default-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/default-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/default-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/default-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-badges.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-badges.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-badges.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-badges.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-cnet-5-stars.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-cnet-5-stars.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-cnet-5-stars.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-cnet-5-stars.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-cnet.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-cnet.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-cnet.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-cnet.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-forbes.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-forbes.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-forbes.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-forbes.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-us-news.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-us-news.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-us-news.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-us-news.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/review-blurb.component.html b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/review-blurb.component.html rename to apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/review-blurb.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/review-blurb.component.ts rename to apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/review-logo.component.html b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/review-logo.component.html rename to apps/web/src/app/billing/trial-initiation/content/review-logo.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/review-logo.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/review-logo.component.ts rename to apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/secrets-manager-content.component.html b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/secrets-manager-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/secrets-manager-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/secrets-manager-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/teams-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams1-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams2-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams3-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams3-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html similarity index 78% rename from apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html rename to apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html index 0b6e44d4eb6c..dddac598a46d 100644 --- a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html +++ b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html @@ -1,17 +1,4 @@ - - - - - - - - (); constructor( @@ -33,10 +29,10 @@ export abstract class BaseAcceptComponent implements OnInit { protected i18nService: I18nService, protected route: ActivatedRoute, protected authService: AuthService, - protected registerRouteService: RegisterRouteService, ) {} abstract authedHandler(qParams: Params): Promise; + abstract unauthedHandler(qParams: Params): Promise; async ngOnInit() { diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.html b/apps/web/src/app/components/environment-selector/environment-selector.component.html index a1fb1a8a0f34..91675bd4945f 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.html +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.html @@ -6,10 +6,9 @@ [attr.href]=" region == currentRegion ? 'javascript:void(0)' : region.urls.webVault + routeAndParams " - class="pr-4" > diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 8f21dfa2c8bc..be42a9ba34e2 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -52,10 +52,10 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; 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 { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService, Urls, @@ -66,14 +66,22 @@ 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 { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +// eslint-disable-next-line no-restricted-imports -- Needed for DI +import { + UnsupportedWebPushConnectionService, + WebPushConnectionService, +} from "@bitwarden/common/platform/notifications/internal"; import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; +import { NoopSdkLoadService } from "@bitwarden/common/platform/services/sdk/noop-sdk-load.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; /* eslint-disable import/no-restricted-paths -- Implementation for memory storage */ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; @@ -91,7 +99,7 @@ import { KeyService as KeyServiceAbstraction, BiometricsService, } from "@bitwarden/key-management"; -import { LockComponentService } from "@bitwarden/key-management/angular"; +import { LockComponentService } from "@bitwarden/key-management-ui"; import { flagEnabled } from "../../utils/flags"; import { PolicyListService } from "../admin-console/core/policy-list.service"; @@ -110,7 +118,7 @@ import { WebProcessReloadService } from "../key-management/services/web-process- import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; -import { WebSdkClientFactory } from "../platform/web-sdk-client-factory"; +import { WebSdkLoadService } from "../platform/web-sdk-load.service"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; import { EventService } from "./event.service"; @@ -242,6 +250,12 @@ const safeProviders: SafeProvider[] = [ PolicyService, ], }), + safeProvider({ + provide: WebPushConnectionService, + // We can support web in the future by creating a worker + useClass: UnsupportedWebPushConnectionService, + deps: [], + }), safeProvider({ provide: LockComponentService, useClass: WebLockComponentService, @@ -288,9 +302,14 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultCollectionAdminService, deps: [ApiService, KeyServiceAbstraction, EncryptService, CollectionService], }), + safeProvider({ + provide: SdkLoadService, + useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService, + deps: [], + }), safeProvider({ provide: SdkClientFactory, - useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory, + useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory, deps: [], }), safeProvider({ diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index b3e6d691f75d..3623d9b0d2f7 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -5,12 +5,13 @@ import { firstValueFrom } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; @@ -23,7 +24,7 @@ import { VersionService } from "../platform/version.service"; export class InitService { constructor( @Inject(WINDOW) private win: Window, - private notificationsService: NotificationsServiceAbstraction, + private notificationsService: NotificationsService, private vaultTimeoutService: VaultTimeoutService, private i18nService: I18nServiceAbstraction, private eventUploadService: EventUploadServiceAbstraction, @@ -35,11 +36,13 @@ export class InitService { private userAutoUnlockKeyService: UserAutoUnlockKeyService, private accountService: AccountService, private versionService: VersionService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.stateService.init(); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); @@ -49,7 +52,7 @@ export class InitService { await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); } - setTimeout(() => this.notificationsService.init(), 3000); + this.notificationsService.startListening(); await this.vaultTimeoutService.init(true); await this.i18nService.init(); (this.eventUploadService as EventUploadService).init(true); diff --git a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts index d407a709d4ca..9358c4b200ee 100644 --- a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts +++ b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts @@ -2,8 +2,14 @@ // @ts-strict-ignore import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/folder-with-id.request"; import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request"; diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index 4f2ae8f77e08..0101bc5aa973 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -7,8 +7,8 @@ import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-con 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"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request"; diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index ae47798420ed..1acbc2012c59 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -8,7 +8,7 @@ import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractio import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; diff --git a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts index 02910966d6e0..8b9212373b91 100644 --- a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts @@ -7,7 +7,7 @@ import { } from "@bitwarden/auth/common"; import { UserId } from "@bitwarden/common/types/guid"; import { BiometricsStatus } from "@bitwarden/key-management"; -import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular"; +import { LockComponentService, UnlockOptions } from "@bitwarden/key-management-ui"; export class WebLockComponentService implements LockComponentService { private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); diff --git a/apps/web/src/app/layouts/frontend-layout.component.html b/apps/web/src/app/layouts/frontend-layout.component.html index 72f0f1f1da39..d19af54f5dff 100644 --- a/apps/web/src/app/layouts/frontend-layout.component.html +++ b/apps/web/src/app/layouts/frontend-layout.component.html @@ -1,6 +1,8 @@ -
+ +
- © {{ year }} Bitwarden Inc.
- {{ "versionNumber" | i18n: version }} -
+ +
© {{ year }} Bitwarden Inc.
+
{{ version }}
+ diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index f34d32f5983b..ef702c7e5936 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -10,7 +10,7 @@ @@ -27,7 +27,7 @@ diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts index b09b32d060e0..d64e1b817c18 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts @@ -3,11 +3,12 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, map, Observable } from "rxjs"; +import { combineLatest, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import type { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { DialogService, NavigationModule } from "@bitwarden/components"; @@ -20,12 +21,17 @@ import { TrialFlowService } from "./../../billing/services/trial-flow.service"; imports: [CommonModule, JslibModule, NavigationModule], }) export class OrgSwitcherComponent { - protected organizations$: Observable = - this.organizationService.organizations$.pipe( - map((orgs) => - orgs.filter((org) => this.filter(org)).sort((a, b) => a.name.localeCompare(b.name)), - ), - ); + protected organizations$: Observable = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe( + map((orgs) => + orgs.filter((org) => this.filter(org)).sort((a, b) => a.name.localeCompare(b.name)), + ), + ), + ), + ); protected activeOrganization$: Observable = combineLatest([ this.route.paramMap, @@ -61,6 +67,7 @@ export class OrgSwitcherComponent { private organizationService: OrganizationService, private trialFlowService: TrialFlowService, protected billingApiService: BillingApiServiceAbstraction, + private accountService: AccountService, ) {} protected toggle(event?: MouseEvent) { diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index 382ce8e026b8..6e21c6c142a9 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -7,6 +7,8 @@ import { BehaviorSubject } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { IconButtonModule, NavigationModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component"; import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index 1c15f7cc7c19..fca9063c8cfb 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -1,16 +1,21 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService, Account } 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 { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { LayoutComponent, NavigationModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; import { ProductSwitcherService } from "../shared/product-switcher.service"; @@ -22,11 +27,14 @@ import { NavigationProductSwitcherComponent } from "./navigation-switcher.compon }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); - organizations$ = MockOrganizationService._orgs; // eslint-disable-line rxjs/no-exposed-subjects + + organizations$(): Observable { + return MockOrganizationService._orgs.asObservable(); + } @Input() set mockOrgs(orgs: Organization[]) { - this.organizations$.next(orgs); + MockOrganizationService._orgs.next(orgs); } } @@ -52,6 +60,21 @@ class MockSyncService implements Partial { } } +class MockAccountService implements Partial { + activeAccount$?: Observable = of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }); +} + +class MockPlatformUtilsService implements Partial { + isSelfHost() { + return false; + } +} + @Component({ selector: "story-layout", template: ``, @@ -86,8 +109,10 @@ export default { imports: [NavigationModule, RouterModule, LayoutComponent], providers: [ { provide: OrganizationService, useClass: MockOrganizationService }, + { provide: AccountService, useClass: MockAccountService }, { provide: ProviderService, useClass: MockProviderService }, { provide: SyncService, useClass: MockSyncService }, + { provide: PlatformUtilsService, useClass: MockPlatformUtilsService }, ProductSwitcherService, { provide: I18nPipe, diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts index 801551663945..834571e2cb45 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts @@ -1,5 +1,7 @@ import { AfterViewInit, ChangeDetectorRef, Component, Input } from "@angular/core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IconButtonType } from "@bitwarden/components/src/icon-button/icon-button.component"; import { ProductSwitcherService } from "./shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index 7a4df4bad005..ca27aae45816 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -1,16 +1,21 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService, Account } 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 { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; import { ProductSwitcherContentComponent } from "./product-switcher-content.component"; @@ -22,11 +27,14 @@ import { ProductSwitcherService } from "./shared/product-switcher.service"; }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); - organizations$ = MockOrganizationService._orgs; // eslint-disable-line rxjs/no-exposed-subjects + + organizations$(): Observable { + return MockOrganizationService._orgs.asObservable(); + } @Input() set mockOrgs(orgs: Organization[]) { - this.organizations$.next(orgs); + MockOrganizationService._orgs.next(orgs); } } @@ -52,6 +60,21 @@ class MockSyncService implements Partial { } } +class MockAccountService implements Partial { + activeAccount$?: Observable = of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }); +} + +class MockPlatformUtilsService implements Partial { + isSelfHost() { + return false; + } +} + @Component({ selector: "story-layout", template: ``, @@ -78,11 +101,15 @@ export default { ], imports: [JslibModule, MenuModule, IconButtonModule, LinkModule, RouterModule], providers: [ + { provide: AccountService, useClass: MockAccountService }, + MockAccountService, { provide: OrganizationService, useClass: MockOrganizationService }, MockOrganizationService, { provide: ProviderService, useClass: MockProviderService }, MockProviderService, { provide: SyncService, useClass: MockSyncService }, + { provide: PlatformUtilsService, useClass: MockPlatformUtilsService }, + MockPlatformUtilsService, ProductSwitcherService, { provide: I18nService, @@ -134,7 +161,9 @@ export default { ], } as Meta; -type Story = StoryObj; +type Story = StoryObj< + ProductSwitcherComponent & MockProviderService & MockOrganizationService & MockAccountService +>; const Template: Story = { render: (args) => ({ diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts index 919b3be04248..a1ac434d590d 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts @@ -10,7 +10,12 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { ProductSwitcherService } from "./product-switcher.service"; @@ -19,8 +24,11 @@ describe("ProductSwitcherService", () => { let router: { url: string; events: Observable }; let organizationService: MockProxy; let providerService: MockProxy; + let accountService: FakeAccountService; + let platformUtilsService: MockProxy; let activeRouteParams = convertToParamMap({ organizationId: "1234" }); const getLastSync = jest.fn().mockResolvedValue(new Date("2024-05-14")); + const userId = Utils.newGuid() as UserId; // The service is dependent on the SyncService, which is behind a `setTimeout` // Most of the tests don't need to test this aspect so `advanceTimersByTime` @@ -36,17 +44,22 @@ describe("ProductSwitcherService", () => { router = mock(); organizationService = mock(); providerService = mock(); + accountService = mockAccountServiceWith(userId); + platformUtilsService = mock(); router.url = "/"; router.events = of({}); - organizationService.organizations$ = of([{}] as Organization[]); + organizationService.organizations$.mockReturnValue(of([{}] as Organization[])); providerService.getAll.mockResolvedValue([] as Provider[]); + platformUtilsService.isSelfHost.mockReturnValue(false); TestBed.configureTestingModule({ providers: [ { provide: Router, useValue: router }, { provide: OrganizationService, useValue: organizationService }, { provide: ProviderService, useValue: providerService }, + { provide: AccountService, useValue: accountService }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, { provide: ActivatedRoute, useValue: { @@ -111,13 +124,15 @@ describe("ProductSwitcherService", () => { }); it("is included in bento when there is an organization with SM", async () => { - organizationService.organizations$ = of([ - { - id: "1234", - canAccessSecretsManager: true, - enabled: true, - }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { + id: "1234", + canAccessSecretsManager: true, + enabled: true, + }, + ] as Organization[]), + ); initiateService(); @@ -128,17 +143,39 @@ describe("ProductSwitcherService", () => { }); describe("Admin/Organizations", () => { - it("includes Organizations in other when there are organizations", async () => { + it("includes Organizations with the internal route in other when there are organizations on cloud", async () => { + initiateService(); + + const products = await firstValueFrom(service.products$); + + const organizations = products.other.find((p) => p.name === "Organizations"); + expect(organizations).toBeDefined(); + expect(organizations.marketingRoute.route).toBe("/create-organization"); + expect(organizations.marketingRoute.external).toBe(false); + + expect(products.other.find((p) => p.name === "Organizations")).toBeDefined(); + expect(products.bento.find((p) => p.name === "Admin Console")).toBeUndefined(); + }); + + it("includes Organizations with the external route in other when there are organizations on Self-Host", async () => { + platformUtilsService.isSelfHost.mockReturnValue(true); initiateService(); const products = await firstValueFrom(service.products$); + const organizations = products.other.find((p) => p.name === "Organizations"); + expect(organizations).toBeDefined(); + expect(organizations.marketingRoute.route).toBe("https://bitwarden.com/products/business/"); + expect(organizations.marketingRoute.external).toBe(true); + expect(products.other.find((p) => p.name === "Organizations")).toBeDefined(); expect(products.bento.find((p) => p.name === "Admin Console")).toBeUndefined(); }); it("includes Admin Console in bento when a user has access to it", async () => { - organizationService.organizations$ = of([{ id: "1234", isOwner: true }] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([{ id: "1234", isOwner: true }] as Organization[]), + ); initiateService(); @@ -194,7 +231,9 @@ describe("ProductSwitcherService", () => { }); it("marks Admin Console as active", async () => { - organizationService.organizations$ = of([{ id: "1234", isOwner: true }] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([{ id: "1234", isOwner: true }] as Organization[]), + ); activeRouteParams = convertToParamMap({ organizationId: "1" }); router.url = "/organizations/"; @@ -225,20 +264,22 @@ describe("ProductSwitcherService", () => { it("updates secrets manager path when the org id is found in the path", async () => { router.url = "/sm/4243"; - organizationService.organizations$ = of([ - { - id: "23443234", - canAccessSecretsManager: true, - enabled: true, - name: "Org 2", - }, - { - id: "4243", - canAccessSecretsManager: true, - enabled: true, - name: "Org 32", - }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { + id: "23443234", + canAccessSecretsManager: true, + enabled: true, + name: "Org 2", + }, + { + id: "4243", + canAccessSecretsManager: true, + enabled: true, + name: "Org 32", + }, + ] as Organization[]), + ); initiateService(); @@ -253,10 +294,12 @@ describe("ProductSwitcherService", () => { it("updates admin console path when the org id is found in the path", async () => { router.url = "/organizations/111-22-33"; - organizationService.organizations$ = of([ - { id: "111-22-33", isOwner: true, name: "Test Org" }, - { id: "4243", isOwner: true, name: "My Org" }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { id: "111-22-33", isOwner: true, name: "Test Org" }, + { id: "4243", isOwner: true, name: "My Org" }, + ] as Organization[]), + ); initiateService(); diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts index 2c16886a2d4c..1cc5c92c1201 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts @@ -2,7 +2,16 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { ActivatedRoute, NavigationEnd, NavigationStart, ParamMap, Router } from "@angular/router"; -import { combineLatest, concatMap, filter, map, Observable, ReplaySubject, startWith } from "rxjs"; +import { + combineLatest, + concatMap, + filter, + map, + Observable, + ReplaySubject, + startWith, + switchMap, +} from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { @@ -11,6 +20,8 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; export type ProductSwitcherItem = { @@ -90,18 +101,21 @@ export class ProductSwitcherService { private router: Router, private i18n: I18nPipe, private syncService: SyncService, + private accountService: AccountService, + private platformUtilsService: PlatformUtilsService, ) { this.pollUntilSynced(); } + organizations$ = this.accountService.activeAccount$.pipe( + map((a) => a?.id), + switchMap((id) => this.organizationService.organizations$(id)), + ); + products$: Observable<{ bento: ProductSwitcherItem[]; other: ProductSwitcherItem[]; - }> = combineLatest([ - this.organizationService.organizations$, - this.route.paramMap, - this.triggerProductUpdate$, - ]).pipe( + }> = combineLatest([this.organizations$, this.route.paramMap, this.triggerProductUpdate$]).pipe( map(([orgs, ...rest]): [Organization[], ParamMap, void] => { return [ // Sort orgs by name to match the order within the sidebar @@ -139,6 +153,16 @@ export class ProductSwitcherService { // TODO: This should be migrated to an Observable provided by the provider service and moved to the combineLatest above. See AC-2092. const providers = await this.providerService.getAll(); + const orgsMarketingRoute = this.platformUtilsService.isSelfHost() + ? { + route: "https://bitwarden.com/products/business/", + external: true, + } + : { + route: "/create-organization", + external: false, + }; + const products = { pm: { name: "Password Manager", @@ -185,10 +209,7 @@ export class ProductSwitcherService { orgs: { name: "Organizations", icon: "bwi-business", - marketingRoute: { - route: "https://bitwarden.com/products/business/", - external: true, - }, + marketingRoute: orgsMarketingRoute, otherProductOverrides: { name: "Share your passwords", supportingText: this.i18n.transform("protectYourFamilyOrBusiness"), diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index f0ac3ef9b48c..e859993af320 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -31,7 +31,6 @@ import { WebLayoutModule } from "./web-layout.module"; }) export class UserLayoutComponent implements OnInit { protected readonly logo = PasswordManagerLogo; - isFreeFamilyFlagEnabled: boolean; protected hasFamilySponsorshipAvailable$: Observable; protected showSponsoredFamilies$: Observable; protected showSubscription$: Observable; diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index fadcc28f832a..5f2b839ae978 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, @@ -9,9 +9,10 @@ import { redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, + activeAuthGuard, } 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 { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -37,16 +38,17 @@ import { SsoComponent, VaultIcon, LoginDecryptionOptionsComponent, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { LockComponent } from "@bitwarden/key-management/angular"; +import { LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, VaultIcons, } from "@bitwarden/vault"; -import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { flagEnabled, Flags } from "../utils/flags"; import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component"; @@ -69,9 +71,6 @@ import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emerg import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component"; import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module"; import { SsoComponentV1 } from "./auth/sso-v1.component"; -import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component"; -import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; -import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component"; import { TwoFactorAuthComponent } from "./auth/two-factor-auth.component"; import { TwoFactorComponent } from "./auth/two-factor.component"; import { UpdatePasswordComponent } from "./auth/update-password.component"; @@ -79,6 +78,8 @@ import { UpdateTempPasswordComponent } from "./auth/update-temp-password.compone import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "./auth/verify-recover-delete.component"; import { SponsoredFamiliesComponent } from "./billing/settings/sponsored-families.component"; +import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component"; +import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component"; import { RouteDataProperties } from "./core"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; @@ -88,14 +89,24 @@ import { SMLandingComponent } from "./secrets-manager/secrets-manager-landing/sm import { DomainRulesComponent } from "./settings/domain-rules.component"; import { PreferencesComponent } from "./settings/preferences.component"; import { CredentialGeneratorComponent } from "./tools/credential-generator/credential-generator.component"; -import { GeneratorComponent } from "./tools/generator.component"; import { ReportsModule } from "./tools/reports"; -import { AccessComponent } from "./tools/send/access.component"; -import { SendAccessExplainerComponent } from "./tools/send/send-access-explainer.component"; +import { AccessComponent, SendAccessExplainerComponent } from "./tools/send/send-access"; import { SendComponent } from "./tools/send/send.component"; import { VaultModule } from "./vault/individual-vault/vault.module"; const routes: Routes = [ + // These need to be placed at the top of the list prior to the root + // so that the redirectGuard does not interrupt the navigation. + { + path: "register", + redirectTo: "signup", + pathMatch: "full", + }, + { + path: "trial", + redirectTo: "signup", + pathMatch: "full", + }, { path: "", component: FrontendLayoutComponent, @@ -112,20 +123,6 @@ const routes: Routes = [ component: LoginViaWebAuthnComponent, data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties, }, - { - path: "register", - component: TrialInitiationComponent, - canActivate: [ - canAccessFeature(FeatureFlag.EmailVerification, false, "/signup", false), - unauthGuardFn(), - ], - data: { titleId: "createAccount" } satisfies RouteDataProperties, - }, - { - path: "trial", - redirectTo: "register", - pathMatch: "full", - }, { path: "set-password", component: SetPasswordComponent, @@ -187,7 +184,7 @@ const routes: Routes = [ data: { pageIcon: DevicesIcon, pageTitle: { - key: "loginInitiated", + key: "logInRequestSent", }, pageSubtitle: { key: "aNotificationWasSentToYourDevice", @@ -347,7 +344,7 @@ const routes: Routes = [ children: [ { path: "signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationUserAddIcon, pageTitle: { @@ -372,7 +369,7 @@ const routes: Routes = [ }, { path: "finish-signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationLockAltIcon, titleId: "setAStrongPassword", @@ -406,7 +403,6 @@ const routes: Routes = [ }, { path: "set-password-jit", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { pageTitle: { @@ -419,7 +415,7 @@ const routes: Routes = [ }, { path: "signup-link-expired", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationExpiredLinkIcon, pageTitle: { @@ -544,12 +540,12 @@ const routes: Routes = [ } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { - path: "2fa-timeout", + path: "authentication-timeout", canActivate: [unauthGuardFn()], children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, { path: "", @@ -586,6 +582,29 @@ const routes: Routes = [ titleId: "recoverAccountTwoStep", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, + { + path: "device-verification", + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [ + { + path: "", + component: NewDeviceVerificationComponent, + }, + ], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, { path: "accept-emergency", canActivate: [deepLinkGuard()], @@ -656,7 +675,7 @@ const routes: Routes = [ }, { path: "trial-initiation", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], component: CompleteTrialInitiationComponent, resolve: { pageTitle: freeTrialTextResolver, @@ -667,7 +686,7 @@ const routes: Routes = [ }, { path: "secrets-manager-trial-initiation", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], component: CompleteTrialInitiationComponent, resolve: { pageTitle: freeTrialTextResolver, @@ -809,10 +828,11 @@ const routes: Routes = [ titleId: "exportVault", } satisfies RouteDataProperties, }, - ...generatorSwap(GeneratorComponent, CredentialGeneratorComponent, { + { path: "generator", + component: CredentialGeneratorComponent, data: { titleId: "generator" } satisfies RouteDataProperties, - }), + }, ], }, { diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 3f18440d2313..39d0a9ae2023 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -2,9 +2,9 @@ import { NgModule } from "@angular/core"; import { AuthModule } from "./auth"; import { LoginModule } from "./auth/login/login.module"; -import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module"; +import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module"; import { LooseComponentsModule, SharedModule } from "./shared"; -import { AccessComponent } from "./tools/send/access.component"; +import { AccessComponent } from "./tools/send/send-access/access.component"; import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module"; import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-filter.module"; diff --git a/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts b/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts new file mode 100644 index 000000000000..44866285251b --- /dev/null +++ b/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts @@ -0,0 +1,54 @@ +import { concat, defer, fromEvent, map, Observable, of, switchMap } from "rxjs"; + +import { SupportStatus } from "@bitwarden/common/platform/misc/support-status"; +// eslint-disable-next-line no-restricted-imports -- In platform owned code. +import { + WebPushConnector, + WorkerWebPushConnectionService, +} from "@bitwarden/common/platform/notifications/internal"; +import { UserId } from "@bitwarden/common/types/guid"; + +export class PermissionsWebPushConnectionService extends WorkerWebPushConnectionService { + override supportStatus$(userId: UserId): Observable> { + return this.notificationPermission$().pipe( + switchMap((notificationPermission) => { + if (notificationPermission === "denied") { + return of>({ + type: "not-supported", + reason: "permission-denied", + }); + } + + if (notificationPermission === "default") { + return of>({ + type: "needs-configuration", + reason: "permission-not-requested", + }); + } + + if (notificationPermission === "prompt") { + return of>({ + type: "needs-configuration", + reason: "prompt-must-be-granted", + }); + } + + // Delegate to default worker checks + return super.supportStatus$(userId); + }), + ); + } + + private notificationPermission$() { + return concat( + of(Notification.permission), + defer(async () => { + return await window.navigator.permissions.query({ name: "notifications" }); + }).pipe( + switchMap((permissionStatus) => { + return fromEvent(permissionStatus, "change").pipe(map(() => permissionStatus.state)); + }), + ), + ); + } +} diff --git a/apps/web/src/app/platform/web-sdk-client-factory.ts b/apps/web/src/app/platform/web-sdk-client-factory.ts deleted file mode 100644 index 0dd43ecbb925..000000000000 --- a/apps/web/src/app/platform/web-sdk-client-factory.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; -import * as sdk from "@bitwarden/sdk-internal"; - -/** - * SDK client factory with a js fallback for when WASM is not supported. - */ -export class WebSdkClientFactory implements SdkClientFactory { - async createSdkClient( - ...args: ConstructorParameters - ): Promise { - const module = await load(); - - (sdk as any).init(module); - - return Promise.resolve(new sdk.BitwardenClient(...args)); - } -} - -// https://stackoverflow.com/a/47880734 -const supported = (() => { - try { - if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { - const module = new WebAssembly.Module( - Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), - ); - if (module instanceof WebAssembly.Module) { - return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; - } - } - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - // ignore - } - return false; -})(); - -async function load() { - if (supported) { - return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); - } else { - return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); - } -} diff --git a/apps/web/src/app/platform/web-sdk-load.service.ts b/apps/web/src/app/platform/web-sdk-load.service.ts new file mode 100644 index 000000000000..cae3399b81e4 --- /dev/null +++ b/apps/web/src/app/platform/web-sdk-load.service.ts @@ -0,0 +1,31 @@ +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import * as sdk from "@bitwarden/sdk-internal"; + +// https://stackoverflow.com/a/47880734 +const supported = (() => { + try { + if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { + const module = new WebAssembly.Module( + Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), + ); + if (module instanceof WebAssembly.Module) { + return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } + } catch { + // ignore + } + return false; +})(); + +export class WebSdkLoadService implements SdkLoadService { + async load(): Promise { + let module: any; + if (supported) { + module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); + } else { + module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); + } + (sdk as any).init(module); + } +} diff --git a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts index 805745c369d0..5edd8bc046e5 100644 --- a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts +++ b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Guid } from "@bitwarden/common/src/types/guid"; +import { Guid } from "@bitwarden/common/types/guid"; export class RequestSMAccessRequest { OrganizationId: Guid; diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts index 8909df6cf8a2..d1ab7689cfe5 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts @@ -3,9 +3,12 @@ import { Component, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { Router } from "@angular/router"; +import { firstValueFrom } 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Guid } from "@bitwarden/common/types/guid"; import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components"; @@ -39,10 +42,12 @@ export class RequestSMAccessComponent implements OnInit { private organizationService: OrganizationService, private smLandingApiService: SmLandingApiService, private toastService: ToastService, + private accountService: AccountService, ) {} async ngOnInit() { - this.organizations = (await this.organizationService.getAll()) + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organizations = (await firstValueFrom(this.organizationService.organizations$(userId))) .filter((e) => e.enabled) .sort((a, b) => a.name.localeCompare(b.name)); diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts index 3698031a5b6d..4d9dceab34ac 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts @@ -1,9 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { NoItemsModule, SearchModule } from "@bitwarden/components"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -22,10 +25,16 @@ export class SMLandingComponent implements OnInit { showSecretsManagerInformation: boolean = true; showGiveMembersAccessInstructions: boolean = false; - constructor(private organizationService: OrganizationService) {} + constructor( + private organizationService: OrganizationService, + private accountService: AccountService, + ) {} async ngOnInit() { - const enabledOrganizations = (await this.organizationService.getAll()).filter((e) => e.enabled); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const enabledOrganizations = ( + await firstValueFrom(this.organizationService.organizations$(userId)) + ).filter((e) => e.enabled); if (enabledOrganizations.length > 0) { this.handleEnabledOrganizations(enabledOrganizations); diff --git a/apps/web/src/app/settings/domain-rules.component.html b/apps/web/src/app/settings/domain-rules.component.html index a3bea63fb863..7e9e7d6ed642 100644 --- a/apps/web/src/app/settings/domain-rules.component.html +++ b/apps/web/src/app/settings/domain-rules.component.html @@ -43,7 +43,7 @@

{{ "customEqDomains" | i18n }}

-

{{ "globalEqDomains" | i18n }}

+

{{ "globalEqDomains" | i18n }}

- - - - {{ "passwordGeneratorPolicyInEffect" | i18n }} - -

-
- -
-
-
- -
- - -
-
- -
- -
- - -
-
- -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
-
- -
-
- - -
-
- - - - {{ passwordOptionsMinLengthForReader$ | async }} - -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- - -
-
- -
-
-
- -
-
- - - - -
-
- - -
-
- -
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- - -
-
-
-
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
-
-
- - -
-
- - diff --git a/apps/web/src/app/tools/generator.component.ts b/apps/web/src/app/tools/generator.component.ts deleted file mode 100644 index a11c0c4a97b9..000000000000 --- a/apps/web/src/app/tools/generator.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; - -import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component"; -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 { DialogService, ToastService } from "@bitwarden/components"; -import { - PasswordGenerationServiceAbstraction, - UsernameGenerationServiceAbstraction, -} from "@bitwarden/generator-legacy"; - -import { PasswordGeneratorHistoryComponent } from "./password-generator-history.component"; - -@Component({ - selector: "app-generator", - templateUrl: "generator.component.html", -}) -export class GeneratorComponent extends BaseGeneratorComponent { - constructor( - passwordGenerationService: PasswordGenerationServiceAbstraction, - usernameGenerationService: UsernameGenerationServiceAbstraction, - accountService: AccountService, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - logService: LogService, - route: ActivatedRoute, - ngZone: NgZone, - private dialogService: DialogService, - toastService: ToastService, - ) { - super( - passwordGenerationService, - usernameGenerationService, - platformUtilsService, - accountService, - i18nService, - logService, - route, - ngZone, - window, - toastService, - ); - if (platformUtilsService.isSelfHost()) { - // Allow only valid email forwarders for self host - this.forwardOptions = this.forwardOptions.filter((forwarder) => forwarder.validForSelfHosted); - } - } - - get isSelfHosted(): boolean { - return this.platformUtilsService.isSelfHost(); - } - - async history() { - this.dialogService.open(PasswordGeneratorHistoryComponent); - } - - lengthChanged() { - document.getElementById("length").focus(); - } - - minNumberChanged() { - document.getElementById("min-number").focus(); - } - - minSpecialChanged() { - document.getElementById("min-special").focus(); - } -} diff --git a/apps/web/src/app/tools/import/import-collection-admin.service.ts b/apps/web/src/app/tools/import/import-collection-admin.service.ts index 093bfed7024a..64050eb9c067 100644 --- a/apps/web/src/app/tools/import/import-collection-admin.service.ts +++ b/apps/web/src/app/tools/import/import-collection-admin.service.ts @@ -1,8 +1,7 @@ import { Injectable } from "@angular/core"; import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; - -import { ImportCollectionServiceAbstraction } from "../../../../../../libs/importer/src/services/import-collection.service.abstraction"; +import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core"; @Injectable() export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction { diff --git a/apps/web/src/app/tools/import/import-web.component.ts b/apps/web/src/app/tools/import/import-web.component.ts index 3f1d51550397..a527b9e71f4f 100644 --- a/apps/web/src/app/tools/import/import-web.component.ts +++ b/apps/web/src/app/tools/import/import-web.component.ts @@ -1,7 +1,7 @@ import { Component } from "@angular/core"; import { Router } from "@angular/router"; -import { ImportComponent } from "@bitwarden/importer/ui"; +import { ImportComponent } from "@bitwarden/importer-ui"; import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; diff --git a/apps/web/src/app/tools/password-generator-history.component.html b/apps/web/src/app/tools/password-generator-history.component.html deleted file mode 100644 index eabb45ece2d6..000000000000 --- a/apps/web/src/app/tools/password-generator-history.component.html +++ /dev/null @@ -1,47 +0,0 @@ - - - {{ "passwordHistory" | i18n }} - - - - - - - - - {{ h.date | date: "medium" }} - - - - - - - - -
- {{ "noPasswordsInList" | i18n }} -
-
- - - - -
diff --git a/apps/web/src/app/tools/password-generator-history.component.ts b/apps/web/src/app/tools/password-generator-history.component.ts deleted file mode 100644 index 0c7c9c4e2217..000000000000 --- a/apps/web/src/app/tools/password-generator-history.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Component } from "@angular/core"; - -import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component"; -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 { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -@Component({ - selector: "app-password-generator-history", - templateUrl: "password-generator-history.component.html", -}) -export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent { - constructor( - passwordGenerationService: PasswordGenerationServiceAbstraction, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - toastService: ToastService, - ) { - super(passwordGenerationService, platformUtilsService, i18nService, window, toastService); - } -} diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index b1a46bd13a85..f78b29204106 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -1,11 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core"; -import { BehaviorSubject, Observable, Subject, takeUntil } from "rxjs"; +import { BehaviorSubject, Observable, Subject, switchMap, takeUntil } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -45,10 +46,13 @@ export class CipherReportComponent implements OnDestroy { private modalService: ModalService, protected passwordRepromptService: PasswordRepromptService, protected organizationService: OrganizationService, + protected accountService: AccountService, protected i18nService: I18nService, private syncService: SyncService, ) { - this.organizations$ = this.organizationService.organizations$; + this.organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + ); this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { this.organizations = orgs; }); diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index 07dc218bd643..16541bdc1092 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -7,7 +7,11 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("ExposedPasswordsReportComponent", () => { let auditService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { syncServiceMock = mock(); auditService = mock(); organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); + accountService = mockAccountServiceWith(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 TestBed.configureTestingModule({ @@ -44,6 +51,10 @@ describe("ExposedPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index 13d2804c5e59..1a0d4043b74b 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -25,6 +26,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple protected cipherService: CipherService, protected auditService: AuditService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -35,6 +37,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 80760eb5decb..385bda03f288 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -6,8 +6,12 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -20,11 +24,14 @@ describe("InactiveTwoFactorReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); + accountService = mockAccountServiceWith(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 TestBed.configureTestingModule({ @@ -38,6 +45,10 @@ describe("InactiveTwoFactorReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts index 792ad0616f2e..52c52041c9d6 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -27,6 +28,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, private logService: LogService, passwordRepromptService: PasswordRepromptService, @@ -38,6 +40,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 9d16bbb1c62f..6a26cd24fe57 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -6,7 +6,11 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -19,11 +23,15 @@ describe("ReusedPasswordsReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); + accountService = mockAccountServiceWith(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 TestBed.configureTestingModule({ @@ -37,6 +45,10 @@ describe("ReusedPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts index a8806acea137..a5c1c65560bd 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -24,6 +25,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -34,6 +36,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 5f66814fdf1e..7cd159108b86 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -7,7 +7,11 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("UnsecuredWebsitesReportComponent", () => { let organizationService: MockProxy; let syncServiceMock: MockProxy; let collectionService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); collectionService = mock(); + accountService = mockAccountServiceWith(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 TestBed.configureTestingModule({ @@ -40,6 +47,10 @@ describe("UnsecuredWebsitesReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts index 6a1ba1f63335..350e5c039808 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { CollectionService, Collection } from "@bitwarden/admin-console/common"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -22,6 +23,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -33,6 +35,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index bcace60ac0ca..578c220f396d 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -6,8 +6,12 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("WeakPasswordsReportComponent", () => { let passwordStrengthService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { syncServiceMock = mock(); passwordStrengthService = mock(); organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); + accountService = mockAccountServiceWith(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 TestBed.configureTestingModule({ @@ -44,6 +51,10 @@ describe("WeakPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index f3ad6840c8bd..c374ecd0e4a8 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -32,6 +33,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen protected cipherService: CipherService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected organizationService: OrganizationService, + protected accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -42,6 +44,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/reports-routing.module.ts b/apps/web/src/app/tools/reports/reports-routing.module.ts index cad6586bb824..941e6eb7d3de 100644 --- a/apps/web/src/app/tools/reports/reports-routing.module.ts +++ b/apps/web/src/app/tools/reports/reports-routing.module.ts @@ -3,7 +3,7 @@ import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; -import { hasPremiumGuard } from "../../core/guards/has-premium.guard"; +import { hasPremiumGuard } from "../../billing/guards/has-premium.guard"; import { BreachReportComponent } from "./pages/breach-report.component"; import { ExposedPasswordsReportComponent } from "./pages/exposed-passwords-report.component"; diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.ts b/apps/web/src/app/tools/send/send-access-explainer.component.ts deleted file mode 100644 index 756a1068985c..000000000000 --- a/apps/web/src/app/tools/send/send-access-explainer.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from "@angular/core"; - -import { RegisterRouteService } from "@bitwarden/auth/common"; - -import { SharedModule } from "../../shared"; - -@Component({ - selector: "app-send-access-explainer", - templateUrl: "send-access-explainer.component.html", - standalone: true, - imports: [SharedModule], -}) -export class SendAccessExplainerComponent { - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - constructor(private registerRouteService: RegisterRouteService) {} -} diff --git a/apps/web/src/app/tools/send/access.component.html b/apps/web/src/app/tools/send/send-access/access.component.html similarity index 100% rename from apps/web/src/app/tools/send/access.component.html rename to apps/web/src/app/tools/send/send-access/access.component.html diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/send-access/access.component.ts similarity index 93% rename from apps/web/src/app/tools/send/access.component.ts rename to apps/web/src/app/tools/send/send-access/access.component.ts index 80439acd510d..a2922914ba58 100644 --- a/apps/web/src/app/tools/send/access.component.ts +++ b/apps/web/src/app/tools/send/send-access/access.component.ts @@ -5,9 +5,7 @@ import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -23,7 +21,7 @@ import { NoItemsModule, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { ExpiredSendIcon } from "@bitwarden/send-ui"; -import { SharedModule } from "../../shared"; +import { SharedModule } from "../../../shared"; import { SendAccessFileComponent } from "./send-access-file.component"; import { SendAccessPasswordComponent } from "./send-access-password.component"; @@ -58,9 +56,6 @@ export class AccessComponent implements OnInit { protected formGroup = this.formBuilder.group({}); - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - private id: string; private key: string; @@ -71,8 +66,6 @@ export class AccessComponent implements OnInit { private sendApiService: SendApiService, private toastService: ToastService, private i18nService: I18nService, - private configService: ConfigService, - private registerRouteService: RegisterRouteService, private layoutWrapperDataService: AnonLayoutWrapperDataService, protected formBuilder: FormBuilder, ) {} diff --git a/apps/web/src/app/tools/send/send-access/index.ts b/apps/web/src/app/tools/send/send-access/index.ts new file mode 100644 index 000000000000..c9df5ce51936 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/index.ts @@ -0,0 +1,2 @@ +export { AccessComponent } from "./access.component"; +export { SendAccessExplainerComponent } from "./send-access-explainer.component"; diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.html b/apps/web/src/app/tools/send/send-access/send-access-explainer.component.html similarity index 84% rename from apps/web/src/app/tools/send/send-access-explainer.component.html rename to apps/web/src/app/tools/send/send-access/send-access-explainer.component.html index e8090cb850c5..f56b68cc2997 100644 --- a/apps/web/src/app/tools/send/send-access-explainer.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-explainer.component.html @@ -10,7 +10,7 @@ >Bitwarden Send {{ "sendAccessTaglineOr" | i18n }} - {{ + {{ "sendAccessTaglineSignUp" | i18n }} {{ "sendAccessTaglineTryToday" | i18n }} diff --git a/apps/web/src/app/tools/send/send-access/send-access-explainer.component.ts b/apps/web/src/app/tools/send/send-access/send-access-explainer.component.ts new file mode 100644 index 000000000000..ec39d9704447 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-access-explainer.component.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +import { SharedModule } from "../../../shared"; + +@Component({ + selector: "app-send-access-explainer", + templateUrl: "send-access-explainer.component.html", + standalone: true, + imports: [SharedModule], +}) +export class SendAccessExplainerComponent { + constructor() {} +} diff --git a/apps/web/src/app/tools/send/send-access-file.component.html b/apps/web/src/app/tools/send/send-access/send-access-file.component.html similarity index 100% rename from apps/web/src/app/tools/send/send-access-file.component.html rename to apps/web/src/app/tools/send/send-access/send-access-file.component.html diff --git a/apps/web/src/app/tools/send/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access/send-access-file.component.ts similarity index 94% rename from apps/web/src/app/tools/send/send-access-file.component.ts rename to apps/web/src/app/tools/send/send-access/send-access-file.component.ts index b55e955f3554..eec0bfd787b6 100644 --- a/apps/web/src/app/tools/send/send-access-file.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-file.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, Input } from "@angular/core"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -13,7 +13,7 @@ import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-ac import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { ToastService } from "@bitwarden/components"; -import { SharedModule } from "../../shared"; +import { SharedModule } from "../../../shared"; @Component({ selector: "app-send-access-file", diff --git a/apps/web/src/app/tools/send/send-access-password.component.html b/apps/web/src/app/tools/send/send-access/send-access-password.component.html similarity index 100% rename from apps/web/src/app/tools/send/send-access-password.component.html rename to apps/web/src/app/tools/send/send-access/send-access-password.component.html diff --git a/apps/web/src/app/tools/send/send-access-password.component.ts b/apps/web/src/app/tools/send/send-access/send-access-password.component.ts similarity index 95% rename from apps/web/src/app/tools/send/send-access-password.component.ts rename to apps/web/src/app/tools/send/send-access/send-access-password.component.ts index bd98e9d18c8b..0cfd93fcea0d 100644 --- a/apps/web/src/app/tools/send/send-access-password.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-password.component.ts @@ -4,7 +4,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angu import { FormBuilder, Validators } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; -import { SharedModule } from "../../shared"; +import { SharedModule } from "../../../shared"; @Component({ selector: "app-send-access-password", diff --git a/apps/web/src/app/tools/send/send-access-text.component.html b/apps/web/src/app/tools/send/send-access/send-access-text.component.html similarity index 100% rename from apps/web/src/app/tools/send/send-access-text.component.html rename to apps/web/src/app/tools/send/send-access/send-access-text.component.html diff --git a/apps/web/src/app/tools/send/send-access-text.component.ts b/apps/web/src/app/tools/send/send-access/send-access-text.component.ts similarity index 97% rename from apps/web/src/app/tools/send/send-access-text.component.ts rename to apps/web/src/app/tools/send/send-access/send-access-text.component.ts index 6568fe482adb..6f9bc798d4b8 100644 --- a/apps/web/src/app/tools/send/send-access-text.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.ts @@ -8,7 +8,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { ToastService } from "@bitwarden/components"; -import { SharedModule } from "../../shared"; +import { SharedModule } from "../../../shared"; @Component({ selector: "app-send-access-text", diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index fbe0649c7aa2..61fc290f6fed 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -70,11 +70,11 @@
- {{ "grantAddAccessCollectionWarning" | i18n }} + {{ "grantManageCollectionWarning" | i18n }} {{ "grantCollectionAccess" | i18n }} {{ diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 42d033dc4c20..a035202516af 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -5,6 +5,7 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angula import { AbstractControl, FormBuilder, Validators } from "@angular/forms"; import { combineLatest, + firstValueFrom, map, Observable, of, @@ -24,12 +25,17 @@ import { CollectionResponse, CollectionView, } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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 { getUserId } from "@bitwarden/common/auth/services/account.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 { BitValidators, DialogService } from "@bitwarden/components"; +import { BitValidators, DialogService, ToastService } from "@bitwarden/components"; import { GroupApiService, GroupView } from "../../../admin-console/organizations/core"; import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component"; @@ -110,6 +116,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private organizationUserApiService: OrganizationUserApiService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, + private accountService: AccountService, + private toastService: ToastService, ) { this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info; } @@ -121,7 +129,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.formGroup.controls.selectedOrg.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe((id) => this.loadOrg(id)); - this.organizations$ = this.organizationService.organizations$.pipe( + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organizations$ = this.organizationService.organizations$(userId).pipe( first(), map((orgs) => orgs @@ -139,8 +150,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { } async loadOrg(orgId: string) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const organization$ = this.organizationService - .get$(orgId) + .organizations$(userId) + .pipe(getOrganizationById(orgId)) .pipe(shareReplay({ refCount: true, bufferSize: 1 })); const groups$ = organization$.pipe( switchMap((organization) => { @@ -274,17 +287,20 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { const accessTabError = this.formGroup.controls.access.hasError("managePermissionRequired"); if (this.tabIndex === CollectionDialogTabType.Access && !accessTabError) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo")), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t( + "fieldOnTabRequiresAttention", + this.i18nService.t("collectionInfo"), + ), + }); } else if (this.tabIndex === CollectionDialogTabType.Info && accessTabError) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access")), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access")), + }); } return; } @@ -309,14 +325,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { const savedCollection = await this.collectionAdminService.save(collectionView); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t( + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( this.editMode ? "editedCollectionId" : "createdCollectionId", collectionView.name, ), - ); + }); this.close(CollectionDialogAction.Saved, savedCollection); }; @@ -339,11 +355,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { await this.collectionAdminService.delete(this.params.organizationId, this.params.collectionId); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deletedCollectionId", this.collection?.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedCollectionId", this.collection?.name), + }); this.close(CollectionDialogAction.Deleted, this.collection); }; diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html index 56acc421de40..45df670570d1 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html @@ -36,7 +36,10 @@
- + + - +
diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 59638aad6531..eb2289d72294 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -4,7 +4,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, Observable, Subject, switchMap } from "rxjs"; +import { firstValueFrom, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; @@ -36,6 +36,7 @@ import { } from "@bitwarden/components"; import { CipherAttachmentsComponent, + CipherFormComponent, CipherFormConfig, CipherFormGenerationService, CipherFormModule, @@ -49,6 +50,8 @@ import { AttachmentDialogResult, AttachmentsV2Component, } from "../../individual-vault/attachments-v2.component"; +import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; +import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service"; @@ -82,6 +85,11 @@ export interface VaultItemDialogParams { * If true, the dialog is being opened from the admin console. */ isAdminConsoleAction?: boolean; + + /** + * Function to restore a cipher from the trash. + */ + restore: (c: CipherView) => Promise; } export enum VaultItemDialogResult { @@ -99,6 +107,11 @@ export enum VaultItemDialogResult { * The dialog was closed to navigate the user the premium upgrade page. */ PremiumUpgrade = "premiumUpgrade", + + /** + * A cipher was restored + */ + Restored = "restored", } @Component({ @@ -121,6 +134,7 @@ export enum VaultItemDialogResult { { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, + RoutedVaultFilterService, ], }) export class VaultItemDialogComponent implements OnInit, OnDestroy { @@ -131,6 +145,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { @ViewChild("dialogContent") protected dialogContent: ElementRef; + @ViewChild(CipherFormComponent) cipherFormComponent!: CipherFormComponent; + /** * Tracks if the cipher was ever modified while the dialog was open. Used to ensure the dialog emits the correct result * in case of closing with the X button or ESC key. @@ -191,6 +207,28 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ), ); + protected get isTrashFilter() { + return this.filter?.type === "trash"; + } + + protected get showCancel() { + return !this.isTrashFilter && !this.showCipherView; + } + + protected get showClose() { + return this.isTrashFilter && !this.showRestore; + } + + /** + * Determines if the user may restore the item. + * A user may restore items if they have delete permissions and the item is in the trash. + */ + protected async canUserRestore() { + return this.isTrashFilter && this.cipher?.isDeleted && this.canDelete; + } + + protected showRestore: boolean; + protected get loadingForm() { return this.loadForm && !this.formReady; } @@ -199,8 +237,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return this.params.disableForm; } - protected get canDelete() { - return this.cipher?.edit ?? false; + protected get showEdit() { + return this.showCipherView && !this.isTrashFilter && !this.showRestore; } protected get showDelete() { @@ -228,7 +266,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected formConfig: CipherFormConfig = this.params.formConfig; - protected canDeleteCipher$: Observable; + protected filter: RoutedVaultFilterModel; + + protected canDelete = false; constructor( @Inject(DIALOG_DATA) protected params: VaultItemDialogParams, @@ -246,6 +286,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { private cipherAuthorizationService: CipherAuthorizationService, private apiService: ApiService, private eventCollectionService: EventCollectionService, + private routedVaultFilterService: RoutedVaultFilterService, ) { this.updateTitle(); } @@ -269,10 +310,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { (o) => o.id === this.cipher.organizationId, ); - this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( - this.cipher, - [this.params.activeCollectionId], - this.params.isAdminConsoleAction, + this.canDelete = await firstValueFrom( + this.cipherAuthorizationService.canDeleteCipher$( + this.cipher, + [this.params.activeCollectionId], + this.params.isAdminConsoleAction, + ), ); await this.eventCollectionService.collect( @@ -283,6 +326,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ); } + this.filter = await firstValueFrom(this.routedVaultFilterService.filter$); + + this.showRestore = await this.canUserRestore(); this.performingInitialLoad = false; } @@ -320,6 +366,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { const cipherData = new CipherData(cipherResponse); cipher = new Cipher(cipherData); + + // Update organizationUseTotp from server response + this.cipher.organizationUseTotp = cipher.organizationUseTotp; } // Store the updated cipher so any following edits use the most up to date cipher @@ -336,6 +385,11 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this._formReadySubject.next(); } + restore = async () => { + await this.params.restore(this.cipher); + this.dialogRef.close(VaultItemDialogResult.Restored); + }; + delete = async () => { if (!this.cipher) { return; @@ -394,6 +448,22 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { result.action === AttachmentDialogResult.Removed || result.action === AttachmentDialogResult.Uploaded ) { + const updatedCipher = await this.cipherService.get(this.formConfig.originalCipher?.id); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const updatedCipherView = await updatedCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), + ); + + this.cipherFormComponent.patchCipher((currentCipher) => { + currentCipher.attachments = updatedCipherView.attachments; + currentCipher.revisionDate = updatedCipherView.revisionDate; + + return currentCipher; + }); + this._cipherModified = true; } }; diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 7e59853851ce..befeee43f694 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -22,7 +22,7 @@ [routerLink]="[]" [queryParams]="{ itemId: cipher.id, action: clickAction }" queryParamsHandling="merge" - [replaceUrl]="extensionRefreshEnabled" + [replaceUrl]="true" title="{{ 'editItemWithName' | i18n: cipher.name }}" type="button" appStopProp diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts index 556d3d545941..5f686fcec9ce 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts @@ -1,12 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { firstValueFrom } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -25,11 +22,6 @@ import { RowHeightClass } from "./vault-items.component"; export class VaultCipherRowComponent implements OnInit { protected RowHeightClass = RowHeightClass; - /** - * Flag to determine if the extension refresh feature flag is enabled. - */ - protected extensionRefreshEnabled = false; - @Input() disabled: boolean; @Input() cipher: CipherView; @Input() showOwner: boolean; @@ -42,6 +34,7 @@ export class VaultCipherRowComponent implements OnInit { @Input() collections: CollectionView[]; @Input() viewingOrgVault: boolean; @Input() canEditCipher: boolean; + @Input() canAssignCollections: boolean; @Input() canManageCollection: boolean; @Output() onEvent = new EventEmitter(); @@ -52,27 +45,20 @@ export class VaultCipherRowComponent implements OnInit { protected CipherType = CipherType; private permissionList = getPermissionList(); private permissionPriority = [ - "canManage", - "canEdit", - "canEditExceptPass", - "canView", - "canViewExceptPass", + "manageCollection", + "editItems", + "editItemsHidePass", + "viewItems", + "viewItemsHidePass", ]; protected organization?: Organization; - constructor( - private configService: ConfigService, - private i18nService: I18nService, - ) {} + constructor(private i18nService: I18nService) {} /** * Lifecycle hook for component initialization. - * Checks if the extension refresh feature flag is enabled to provide to template. */ async ngOnInit(): Promise { - this.extensionRefreshEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh), - ); if (this.cipher.organizationId != null) { this.organization = this.organizations.find((o) => o.id === this.cipher.organizationId); } @@ -82,7 +68,7 @@ export class VaultCipherRowComponent implements OnInit { if (this.cipher.decryptionFailure) { return "showFailedToDecrypt"; } - return this.extensionRefreshEnabled ? "view" : null; + return "view"; } protected get showTotpCopyButton() { @@ -101,7 +87,7 @@ export class VaultCipherRowComponent implements OnInit { } protected get showAssignToCollections() { - return this.organizations?.length && this.canEditCipher && !this.cipher.isDeleted; + return this.organizations?.length && this.canAssignCollections && !this.cipher.isDeleted; } protected get showClone() { @@ -118,7 +104,7 @@ export class VaultCipherRowComponent implements OnInit { protected get permissionText() { if (!this.cipher.organizationId || this.cipher.collectionIds.length === 0) { - return this.i18nService.t("canManage"); + return this.i18nService.t("manageCollection"); } const filteredCollections = this.collections.filter((collection) => { @@ -208,6 +194,6 @@ export class VaultCipherRowComponent implements OnInit { return true; // Always show checkbox in individual vault or for non-org items } - return this.organization.canEditAllCiphers || this.cipher.edit; + return this.organization.canEditAllCiphers || (this.cipher.edit && this.cipher.viewPassword); } } diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 404e26fc2106..d07ba46d1363 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -74,7 +74,7 @@ export class VaultCollectionRowComponent { get permissionText() { if (this.collection.id == Unassigned && this.organization?.canEditUnassignedCiphers) { - return this.i18nService.t("canEdit"); + return this.i18nService.t("editItems"); } if ((this.collection as CollectionAdminView).assigned) { const permissionList = getPermissionList(); diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 653d05ef1296..a32def5fc0c3 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -144,6 +144,7 @@ [collections]="allCollections" [checked]="selection.isSelected(item)" [canEditCipher]="canEditCipher(item.cipher)" + [canAssignCollections]="canAssignCollections(item.cipher)" [canManageCollection]="canManageCollection(item.cipher)" (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 3e1cf173a476..a641c5b59089 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -236,6 +236,13 @@ export class VaultItemsComponent { return (organization.canEditAllCiphers && this.viewingOrgVault) || cipher.edit; } + protected canAssignCollections(cipher: CipherView) { + const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); + return ( + (organization?.canEditAllCiphers && this.viewingOrgVault) || cipher.canAssignToCollections + ); + } + protected canManageCollection(cipher: CipherView) { // If the cipher is not part of an organization (personal item), user can manage it if (cipher.organizationId == null) { @@ -461,7 +468,7 @@ export class VaultItemsComponent { private allCiphersHaveEditAccess(): boolean { return this.selection.selected .filter(({ cipher }) => cipher) - .every(({ cipher }) => cipher?.edit); + .every(({ cipher }) => cipher?.edit && cipher?.viewPassword); } private getUniqueOrganizationIds(): Set { diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts index 1ca9b0de47c3..9127a213a45d 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts @@ -49,7 +49,7 @@ describe("AddEditComponentV2", () => { } as Organization; organizationService = mock(); - organizationService.organizations$ = of([mockOrganization]); + organizationService.organizations$.mockReturnValue(of([mockOrganization])); policyService = mock(); policyService.policyAppliesToActiveUser$.mockImplementation((policyType: PolicyType) => diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 56db7dc88da9..a794687c45b3 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -143,11 +143,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On }, 1000); } - const extensionRefreshEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh), - ); - - this.cardIsExpired = extensionRefreshEnabled && isCardExpired(this.cipher.card); + this.cardIsExpired = isCardExpired(this.cipher.card); } ngOnDestroy() { @@ -194,11 +190,11 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On } this.platformUtilsService.copyToClipboard(value, { window: window }); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), + }); if (this.editMode) { if (typeI18nKey === "password") { diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.ts b/apps/web/src/app/vault/individual-vault/attachments.component.ts index a6c25b71fd4c..c6079dbe78f7 100644 --- a/apps/web/src/app/vault/individual-vault/attachments.component.ts +++ b/apps/web/src/app/vault/individual-vault/attachments.component.ts @@ -4,7 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang 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 { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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"; diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 913f106004dd..becfcb8f588b 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -10,7 +10,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherBulkDeleteRequest } from "@bitwarden/common/vault/models/request/cipher-bulk-delete.request"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; export interface BulkDeleteDialogParams { cipherIds?: string[]; @@ -60,6 +60,7 @@ export class BulkDeleteDialogComponent { private i18nService: I18nService, private apiService: ApiService, private collectionService: CollectionService, + private toastService: ToastService, ) { this.cipherIds = params.cipherIds ?? []; this.permanent = params.permanent; @@ -95,19 +96,19 @@ export class BulkDeleteDialogComponent { await Promise.all(deletePromises); if (this.cipherIds.length || this.unassignedCiphers.length) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.permanent ? "permanentlyDeletedItems" : "deletedItems"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.permanent ? "permanentlyDeletedItems" : "deletedItems"), + }); } if (this.collections.length) { await this.collectionService.delete(this.collections.map((c) => c.id)); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deletedCollections"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedCollections"), + }); } this.close(BulkDeleteDialogResult.Deleted); }; @@ -134,11 +135,11 @@ export class BulkDeleteDialogComponent { // From org vault if (this.organization) { if (this.collections.some((c) => !c.canDelete(this.organization))) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("missingPermissions"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("missingPermissions"), + }); return; } return await this.apiService.deleteManyCollections( @@ -151,11 +152,11 @@ export class BulkDeleteDialogComponent { for (const organization of this.organizations) { const orgCollections = this.collections.filter((o) => o.organizationId === organization.id); if (orgCollections.some((c) => !c.canDelete(organization))) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("missingPermissions"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("missingPermissions"), + }); return; } const orgCollectionIds = orgCollections.map((c) => c.id); diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index b7f99fb7b442..295d3ccc4355 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -11,7 +11,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.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 { DialogService, ToastService } from "@bitwarden/components"; export interface BulkMoveDialogParams { cipherIds?: string[]; @@ -58,6 +58,7 @@ export class BulkMoveDialogComponent implements OnInit { private i18nService: I18nService, private folderService: FolderService, private formBuilder: FormBuilder, + private toastService: ToastService, private accountService: AccountService, ) { this.cipherIds = params.cipherIds ?? []; @@ -81,7 +82,11 @@ export class BulkMoveDialogComponent implements OnInit { } await this.cipherService.moveManyWithServer(this.cipherIds, this.formGroup.value.folderId); - this.platformUtilsService.showToast("success", null, this.i18nService.t("movedItems")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("movedItems"), + }); this.close(BulkMoveDialogResult.Moved); }; diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts index 62798c21bca5..2deb5d35341a 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts @@ -8,13 +8,13 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm 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 { getUserId } from "@bitwarden/common/auth/services/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 { Checkable, isChecked } from "@bitwarden/common/types/checkable"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; export interface BulkShareDialogParams { ciphers: CipherView[]; @@ -59,12 +59,12 @@ export class BulkShareDialogComponent implements OnInit, OnDestroy { @Inject(DIALOG_DATA) params: BulkShareDialogParams, private dialogRef: DialogRef, private cipherService: CipherService, - private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private collectionService: CollectionService, private organizationService: OrganizationService, private logService: LogService, private accountService: AccountService, + private toastService: ToastService, ) { this.ciphers = params.ciphers ?? []; this.organizationId = params.organizationId; @@ -77,7 +77,8 @@ export class BulkShareDialogComponent implements OnInit, OnDestroy { this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length; const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.filter((c) => !c.readOnly); - this.organizations = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organizations = await firstValueFrom(this.organizationService.organizations$(userId)); if (this.organizationId == null && this.organizations.length > 0) { this.organizationId = this.organizations[0].id; } @@ -114,11 +115,11 @@ export class BulkShareDialogComponent implements OnInit, OnDestroy { const orgName = this.organizations.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization"); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("movedItemsToOrg", orgName), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("movedItemsToOrg", orgName), + }); this.close(BulkShareDialogResult.Shared); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts index bc639ff2f61c..272cfb130b98 100644 --- a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts @@ -45,6 +45,7 @@ export class FolderAddEditComponent extends BaseFolderAddEditComponent { logService, dialogService, formBuilder, + toastService, ); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-expressions @@ -89,11 +90,11 @@ export class FolderAddEditComponent extends BaseFolderAddEditComponent { const folder = await this.folderService.encrypt(this.folder, userKey); this.formPromise = this.folderApiService.save(folder, activeAccountId); await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), + }); this.onSavedFolder.emit(this.folder); this.dialogRef.close(FolderAddEditDialogResult.Saved); } catch (e) { diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 390b95fa2b11..475cfc2df225 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -204,6 +204,7 @@ export class VaultBannersService { private async isLowKdfIteration(userId: UserId) { const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId)); return ( + kdfConfig != null && kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue ); diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts index 161b2ccb7efd..5a0c0a535b4d 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts @@ -9,7 +9,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { BannerModule } from "@bitwarden/components"; import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component"; -import { FreeTrial } from "../../../core/types/free-trial"; +import { FreeTrial } from "../../../billing/types/free-trial"; import { SharedModule } from "../../../shared"; import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service"; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 6788471dd04e..37e3aca6cd5e 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -1,7 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { combineLatest, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs"; +import { + combineLatest, + firstValueFrom, + map, + Observable, + of, + Subject, + switchMap, + takeUntil, +} from "rxjs"; import { OrganizationUserApiService, @@ -15,7 +24,9 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -60,6 +71,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { private toastService: ToastService, private configService: ConfigService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -67,16 +79,19 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)), ); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const managingOrg$ = this.configService .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) .pipe( switchMap((isAccountDeprovisioningEnabled) => isAccountDeprovisioningEnabled - ? this.organizationService.organizations$.pipe( - map((organizations) => - organizations.find((o) => o.userIsManagedByOrganization === true), - ), - ) + ? this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => + organizations.find((o) => o.userIsManagedByOrganization === true), + ), + ) : of(null), ), ); @@ -146,7 +161,11 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { return this.syncService.fullSync(true); }); await this.actionPromise; - this.platformUtilsService.showToast("success", null, "Unlinked SSO"); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("unlinkedSso"), + }); } catch (e) { this.logService.error(e); } @@ -166,7 +185,11 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { try { this.actionPromise = this.organizationApiService.leave(org.id); await this.actionPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("leftOrganization"), + }); } catch (e) { this.logService.error(e); } @@ -199,11 +222,11 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { ); try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("withdrawPasswordResetSuccess"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("withdrawPasswordResetSuccess"), + }); await this.syncService.fullSync(true); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index f568ba159a66..f1241ef32801 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -13,7 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TrialFlowService } from "../../../../billing/services/trial-flow.service"; import { VaultFilterService } from "../services/abstractions/vault-filter.service"; @@ -98,6 +98,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected toastService: ToastService, protected billingApiService: BillingApiServiceAbstraction, protected dialogService: DialogService, protected configService: ConfigService, @@ -122,11 +123,11 @@ export class VaultFilterComponent implements OnInit, OnDestroy { applyOrganizationFilter = async (orgNode: TreeNode): Promise => { if (!orgNode?.node.enabled) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("disabledOrganizationFilterError"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("disabledOrganizationFilterError"), + }); const metadata = await this.billingApiService.getOrganizationBillingMetadata(orgNode.node.id); await this.trialFlowService.handleUnpaidSubscriptionDialog(orgNode.node, metadata); } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts index b4f52180e521..b8494c8aa541 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts @@ -4,8 +4,8 @@ import { Observable } from "rxjs"; import { CollectionAdminView, CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherTypeFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index 47003d51caeb..8d74f69ed06c 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -62,7 +62,7 @@ describe("vault filter service", () => { personalOwnershipPolicy = new ReplaySubject(1); singleOrgPolicy = new ReplaySubject(1); - organizationService.memberOrganizations$ = organizations; + organizationService.memberOrganizations$.mockReturnValue(organizations); folderService.folderViews$.mockReturnValue(folderViews); collectionService.decryptedCollections$ = collectionViews; policyService.policyAppliesToActiveUser$ diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index 97b44132e601..03dfa92d0b54 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -48,8 +48,12 @@ const NestingDelimiter = "/"; export class VaultFilterService implements VaultFilterServiceAbstraction { private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + memberOrganizations$ = this.activeUserId$.pipe( + switchMap((id) => this.organizationService.memberOrganizations$(id)), + ); + organizationTree$: Observable> = combineLatest([ - this.organizationService.memberOrganizations$, + this.memberOrganizations$, this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg), this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), ]).pipe( @@ -270,6 +274,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { folderCopy.id = f.id; folderCopy.revisionDate = f.revisionDate; folderCopy.icon = "bwi-folder"; + folderCopy.fullName = f.name; // save full folder name before separating it into parts const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter); }); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts index 0cd385bd19d2..9259dd081147 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts @@ -1,8 +1,8 @@ import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; export type CipherStatus = "all" | "favorites" | "trash" | CipherType; @@ -10,5 +10,13 @@ export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: str export type CollectionFilter = CollectionAdminView & { icon: string; }; -export type FolderFilter = FolderView & { icon: string }; +export type FolderFilter = FolderView & { + icon: string; + /** + * Full folder name. + * + * Used for when the folder `name` property is be separated into parts. + */ + fullName?: string; +}; export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean }; diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html index 8ac6138db7c3..8d576098a742 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html @@ -69,88 +69,48 @@
- - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + +
diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 75b39a2ca402..63af397e726c 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -9,13 +9,10 @@ import { OnInit, Output, } from "@angular/core"; -import { firstValueFrom } from "rxjs"; import { Unassigned, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -50,7 +47,6 @@ export class VaultHeaderComponent implements OnInit { protected All = All; protected CollectionDialogTabType = CollectionDialogTabType; protected CipherType = CipherType; - protected extensionRefreshEnabled: boolean; /** * Boolean to determine the loading state of the header. @@ -85,16 +81,9 @@ export class VaultHeaderComponent implements OnInit { /** Emits an event when the delete collection button is clicked in the header */ @Output() onDeleteCollection = new EventEmitter(); - constructor( - private i18nService: I18nService, - private configService: ConfigService, - ) {} + constructor(private i18nService: I18nService) {} - async ngOnInit() { - this.extensionRefreshEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh), - ); - } + async ngOnInit() {} /** * The id of the organization that is currently being filtered on. diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html index b9647e3237df..aa56daac0713 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html @@ -22,12 +22,7 @@

{{ "onboardingImportDataDetailsPartOne" | i18n }} {{ "onboardingImportDataDetailsPartTwoNoOrgs" | i18n }} diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts index e017bc9b35dc..327a077dc6ad 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts @@ -7,7 +7,6 @@ import { Subject, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -28,7 +27,6 @@ describe("VaultOnboardingComponent", () => { let mockStateProvider: Partial; let setInstallExtLinkSpy: any; let individualVaultPolicyCheckSpy: any; - let mockConfigService: MockProxy; beforeEach(() => { mockPolicyService = mock(); @@ -47,7 +45,6 @@ describe("VaultOnboardingComponent", () => { }), ), }; - mockConfigService = mock(); // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -60,7 +57,6 @@ describe("VaultOnboardingComponent", () => { { provide: I18nService, useValue: mockI18nService }, { provide: ApiService, useValue: mockApiService }, { provide: StateProvider, useValue: mockStateProvider }, - { provide: ConfigService, useValue: mockConfigService }, ], }).compileComponents(); fixture = TestBed.createComponent(VaultOnboardingComponent); diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index 3535d31852e2..e3bd8fc10bd3 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -18,8 +18,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum"; @@ -58,14 +56,12 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { protected onboardingTasks$: Observable; protected showOnboarding = false; - protected extensionRefreshEnabled = false; constructor( protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, private apiService: ApiService, private vaultOnboardingService: VaultOnboardingServiceAbstraction, - private configService: ConfigService, ) {} async ngOnInit() { @@ -74,9 +70,6 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { this.setInstallExtLink(); this.individualVaultPolicyCheck(); this.checkForBrowserExtension(); - this.extensionRefreshEnabled = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); } async ngOnChanges(changes: SimpleChanges) { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 8c1d08b269c1..ff8a008bcc57 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,15 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef } from "@angular/cdk/dialog"; -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, - ViewChild, - ViewContainerRef, -} from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; import { BehaviorSubject, @@ -42,21 +34,21 @@ import { Unassigned, } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.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 { + getOrganizationById, + 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 { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; -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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -68,12 +60,13 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DialogService, Icons, ToastService } from "@bitwarden/components"; import { + AddEditFolderDialogComponent, + AddEditFolderDialogResult, CipherFormConfig, CollectionAssignmentResult, DecryptionFailureDialogComponent, @@ -82,7 +75,7 @@ import { } from "@bitwarden/vault"; import { TrialFlowService } from "../../billing/services/trial-flow.service"; -import { FreeTrial } from "../../core/types/free-trial"; +import { FreeTrial } from "../../billing/types/free-trial"; import { SharedModule } from "../../shared/shared.module"; import { AssignCollectionsWebComponent } from "../components/assign-collections"; import { @@ -100,13 +93,11 @@ import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; import { getNestedCollectionTree } from "../utils/collection-utils"; -import { AddEditComponent } from "./add-edit.component"; import { AttachmentDialogCloseResult, AttachmentDialogResult, AttachmentsV2Component, } from "./attachments-v2.component"; -import { AttachmentsComponent } from "./attachments.component"; import { BulkDeleteDialogResult, openBulkDeleteDialog, @@ -115,7 +106,6 @@ import { BulkMoveDialogResult, openBulkMoveDialog, } from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component"; -import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component"; import { VaultBannersComponent } from "./vault-banners/vault-banners.component"; import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component"; import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service"; @@ -156,15 +146,6 @@ const SearchTextDebounceInterval = 200; }) export class VaultComponent implements OnInit, OnDestroy { @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; - @ViewChild("attachments", { read: ViewContainerRef, static: true }) - attachmentsModalRef: ViewContainerRef; - @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) - folderAddEditModalRef: ViewContainerRef; - @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) - cipherAddEditModalRef: ViewContainerRef; - @ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef; - @ViewChild("collectionsModal", { read: ViewContainerRef, static: true }) - collectionsModalRef: ViewContainerRef; trashCleanupWarning: string = null; kdfIterations: number; @@ -189,11 +170,14 @@ export class VaultComponent implements OnInit, OnDestroy { private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); - private extensionRefreshEnabled: boolean; private hasSubscription$ = new BehaviorSubject(false); private vaultItemDialogRef?: DialogRef | undefined; - private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe( + private organizations$ = this.accountService.activeAccount$ + .pipe(map((a) => a?.id)) + .pipe(switchMap((id) => this.organizationService.organizations$(id))); + + private readonly unpaidSubscriptionDialog$ = this.organizations$.pipe( filter((organizations) => organizations.length === 1), map(([organization]) => organization), switchMap((organization) => @@ -212,9 +196,8 @@ export class VaultComponent implements OnInit, OnDestroy { ), ), ); - protected organizationsPaymentStatus$: Observable = combineLatest([ - this.organizationService.organizations$.pipe( + this.organizations$.pipe( map( (organizations) => organizations?.filter((org) => org.isOwner && org.canViewBillingHistory) ?? [], @@ -253,7 +236,6 @@ export class VaultComponent implements OnInit, OnDestroy { private router: Router, private changeDetectorRef: ChangeDetectorRef, private i18nService: I18nService, - private modalService: ModalService, private dialogService: DialogService, private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, @@ -271,7 +253,6 @@ export class VaultComponent implements OnInit, OnDestroy { private eventCollectionService: EventCollectionService, private searchService: SearchService, private searchPipe: SearchPipe, - private configService: ConfigService, private apiService: ApiService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, @@ -430,15 +411,15 @@ export class VaultComponent implements OnInit, OnDestroy { firstSetup$ .pipe( switchMap(() => this.route.queryParams), - // Only process the queryParams if the dialog is not open (only when extension refresh is enabled) - filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled), + // Only process the queryParams if the dialog is not open + filter(() => this.vaultItemDialogRef == undefined), switchMap(async (params) => { const cipherId = getCipherIdFromParams(params); if (cipherId) { if (await this.cipherService.get(cipherId)) { let action = params.action; - // Default to "view" if extension refresh is enabled - if (action == null && this.extensionRefreshEnabled) { + // Default to "view" + if (action == null) { action = "view"; } @@ -501,7 +482,7 @@ export class VaultComponent implements OnInit, OnDestroy { filter$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(this.activeUserId), allCollections$, - this.organizationService.organizations$, + this.organizations$, ciphers$, collections$, selectedCollection$, @@ -537,11 +518,6 @@ export class VaultComponent implements OnInit, OnDestroy { this.refreshing = false; }, ); - - // Check if the extension refresh feature flag is enabled - this.extensionRefreshEnabled = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); } ngOnDestroy() { @@ -601,20 +577,24 @@ export class VaultComponent implements OnInit, OnDestroy { await this.filterComponent.filters?.organizationFilter?.action(orgNode); } - addFolder = async (): Promise => { - openFolderAddEditDialog(this.dialogService); + addFolder = (): void => { + AddEditFolderDialogComponent.open(this.dialogService); }; editFolder = async (folder: FolderFilter): Promise => { - const dialog = openFolderAddEditDialog(this.dialogService, { - data: { - folderId: folder.id, + const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, { + editFolderConfig: { + // Shallow copy is used so the original folder object is not modified + folder: { + ...folder, + name: folder.fullName ?? folder.name, // If the filter has a fullName populated, use that as the editable name + }, }, }); - const result = await lastValueFrom(dialog.closed); + const result = await lastValueFrom(dialogRef.closed); - if (result === FolderAddEditDialogResult.Deleted) { + if (result === AddEditFolderDialogResult.Deleted) { await this.router.navigate([], { queryParams: { folderId: null }, queryParamsHandling: "merge", @@ -631,8 +611,7 @@ export class VaultComponent implements OnInit, OnDestroy { * Handles opening the attachments dialog for a cipher. * Runs several checks to ensure that the user has the correct permissions * and then opens the attachments dialog. - * Uses the new AttachmentsV2Component if the extensionRefresh feature flag is enabled. - * + * Uses the new AttachmentsV2Component * @param cipher * @returns */ @@ -646,7 +625,9 @@ export class VaultComponent implements OnInit, OnDestroy { this.messagingService.send("premiumRequired"); return; } else if (cipher.organizationId != null) { - const org = await this.organizationService.get(cipher.organizationId); + const org = await firstValueFrom( + this.organizations$.pipe(getOrganizationById(cipher.organizationId)), + ); if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) { this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId, @@ -655,51 +636,20 @@ export class VaultComponent implements OnInit, OnDestroy { } } - const canEditAttachments = await this.canEditAttachments(cipher); - - let madeAttachmentChanges = false; - - if (this.extensionRefreshEnabled) { - const dialogRef = AttachmentsV2Component.open(this.dialogService, { - cipherId: cipher.id as CipherId, - }); - - const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed); + const dialogRef = AttachmentsV2Component.open(this.dialogService, { + cipherId: cipher.id as CipherId, + }); - if ( - result.action === AttachmentDialogResult.Uploaded || - result.action === AttachmentDialogResult.Removed - ) { - this.refresh(); - } + const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed); - return; + if ( + result.action === AttachmentDialogResult.Uploaded || + result.action === AttachmentDialogResult.Removed + ) { + this.refresh(); } - const [modal] = await this.modalService.openViewRef( - AttachmentsComponent, - this.attachmentsModalRef, - (comp) => { - comp.cipherId = cipher.id; - comp.viewOnly = !canEditAttachments; - comp.onUploadedAttachment - .pipe(takeUntil(this.destroy$)) - .subscribe(() => (madeAttachmentChanges = true)); - comp.onDeletedAttachment - .pipe(takeUntil(this.destroy$)) - .subscribe(() => (madeAttachmentChanges = true)); - comp.onReuploadedAttachment - .pipe(takeUntil(this.destroy$)) - .subscribe(() => (madeAttachmentChanges = true)); - }, - ); - - modal.onClosed.pipe(takeUntil(this.destroy$)).subscribe(() => { - if (madeAttachmentChanges) { - this.refresh(); - } - madeAttachmentChanges = false; - }); + return; } /** @@ -717,6 +667,7 @@ export class VaultComponent implements OnInit, OnDestroy { mode, formConfig, activeCollectionId, + restore: this.restore, }); const result = await lastValueFrom(this.vaultItemDialogRef.closed); @@ -737,48 +688,13 @@ export class VaultComponent implements OnInit, OnDestroy { await this.go({ cipherId: null, itemId: null, action: null }); } - async addCipher(cipherType?: CipherType) { - const type = cipherType ?? this.activeFilter.cipherType; - - if (this.extensionRefreshEnabled) { - return this.addCipherV2(type); - } - - const component = (await this.editCipher(null)) as AddEditComponent; - component.type = type; - if ( - this.activeFilter.organizationId !== "MyVault" && - this.activeFilter.organizationId != null - ) { - component.organizationId = this.activeFilter.organizationId; - component.collections = ( - await firstValueFrom(this.vaultFilterService.filteredCollections$) - ).filter((c) => !c.readOnly && c.id != null); - } - const selectedColId = this.activeFilter.collectionId; - if (selectedColId !== "AllCollections" && selectedColId != null) { - const selectedCollection = ( - await firstValueFrom(this.vaultFilterService.filteredCollections$) - ).find((c) => c.id === selectedColId); - component.organizationId = selectedCollection?.organizationId; - if (!selectedCollection.readOnly) { - component.collectionIds = [selectedColId]; - } - } - component.folderId = this.activeFilter.folderId; - } - /** * Opens the add cipher dialog. * @param cipherType The type of cipher to add. - * @returns The dialog reference. */ - async addCipherV2(cipherType?: CipherType) { - const cipherFormConfig = await this.cipherFormConfigService.buildConfig( - "add", - null, - cipherType, - ); + async addCipher(cipherType?: CipherType) { + const type = cipherType ?? this.activeFilter.cipherType; + const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", null, type); const collectionId = this.activeFilter.collectionId !== "AllCollections" && this.activeFilter.collectionId != null ? this.activeFilter.collectionId @@ -809,6 +725,12 @@ export class VaultComponent implements OnInit, OnDestroy { return this.editCipherId(cipher?.id, cloneMode); } + /** + * Edit a cipher using the new VaultItemDialog. + * @param id + * @param cloneMode + * @returns + */ async editCipherId(id: string, cloneMode?: boolean) { const cipher = await this.cipherService.get(id); @@ -822,49 +744,6 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - if (this.extensionRefreshEnabled) { - await this.editCipherIdV2(cipher, cloneMode); - return; - } - - const [modal, childComponent] = await this.modalService.openViewRef( - AddEditComponent, - this.cipherAddEditModalRef, - (comp) => { - comp.cipherId = id; - comp.collectionId = this.selectedCollection?.node.id; - - comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - comp.onDeletedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - comp.onRestoredCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - }, - ); - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - modal.onClosedPromise().then(() => { - void this.go({ cipherId: null, itemId: null, action: null }); - }); - - return childComponent; - } - - /** - * Edit a cipher using the new VaultItemDialog. - * - * @param cipher - * @param cloneMode - */ - private async editCipherIdV2(cipher: Cipher, cloneMode?: boolean) { const cipherFormConfig = await this.cipherFormConfigService.buildConfig( cloneMode ? "clone" : "edit", cipher.id as CipherId, @@ -970,7 +849,9 @@ export class VaultComponent implements OnInit, OnDestroy { } async deleteCollection(collection: CollectionView): Promise { - const organization = await this.organizationService.get(collection.organizationId); + const organization = await firstValueFrom( + this.organizations$.pipe(getOrganizationById(collection.organizationId)), + ); if (!collection.canDelete(organization)) { this.showMissingPermissionsError(); return; @@ -1060,14 +941,10 @@ export class VaultComponent implements OnInit, OnDestroy { } } - const component = await this.editCipher(cipher, true); - - if (component != null) { - component.cloneMode = true; - } + await this.editCipher(cipher, true); } - async restore(c: CipherView): Promise { + restore = async (c: CipherView): Promise => { if (!c.isDeleted) { return; } @@ -1092,7 +969,7 @@ export class VaultComponent implements OnInit, OnDestroy { } catch (e) { this.logService.error(e); } - } + }; async bulkRestore(ciphers: CipherView[]) { if (ciphers.some((c) => !c.edit)) { @@ -1135,9 +1012,7 @@ export class VaultComponent implements OnInit, OnDestroy { .filter((i) => i.cipher === undefined) .map((i) => i.collection.organizationId); const orgs = await firstValueFrom( - this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => orgIds.includes(o.id))), - ), + this.organizations$.pipe(map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))), ); await this.bulkDelete(ciphers, collections, orgs); } @@ -1317,15 +1192,6 @@ export class VaultComponent implements OnInit, OnDestroy { this.refresh$.next(); } - private async canEditAttachments(cipher: CipherView) { - if (cipher.organizationId == null || cipher.edit) { - return true; - } - - const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); - return organization.canEditAllCiphers; - } - private async go(queryParams: any = null) { if (queryParams == null) { queryParams = { diff --git a/apps/web/src/app/vault/individual-vault/view.component.spec.ts b/apps/web/src/app/vault/individual-vault/view.component.spec.ts index bde9f564c4a9..9bea7f14eb58 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.spec.ts @@ -1,6 +1,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -11,7 +12,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co 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 { mockAccountServiceWith } from "@bitwarden/common/spec"; +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 { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -41,6 +43,8 @@ describe("ViewComponent", () => { const mockParams: ViewCipherDialogParams = { cipher: mockCipher, }; + const userId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(userId); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -53,10 +57,14 @@ describe("ViewComponent", () => { { provide: CipherService, useValue: mock() }, { provide: ToastService, useValue: mock() }, { provide: MessagingService, useValue: mock() }, + { + provide: AccountService, + useValue: accountService, + }, { provide: LogService, useValue: mock() }, { provide: OrganizationService, - useValue: { get: jest.fn().mockResolvedValue(mockOrganization) }, + useValue: { organizations$: jest.fn().mockReturnValue(of([mockOrganization])) }, }, { provide: CollectionService, useValue: mock() }, { provide: FolderService, useValue: mock() }, diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index e9ca2bf8f8c5..9b6d15c581de 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -3,16 +3,19 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, firstValueFrom, map } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; 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 { getUserId } from "@bitwarden/common/auth/services/account.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 { CollectionId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -23,9 +26,8 @@ import { DialogService, ToastService, } from "@bitwarden/components"; +import { CipherViewComponent } from "@bitwarden/vault"; -import { PremiumUpgradePromptService } from "../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; -import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component"; import { SharedModule } from "../../shared/shared.module"; import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service"; import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service"; @@ -94,6 +96,7 @@ export class ViewComponent implements OnInit { private toastService: ToastService, private organizationService: OrganizationService, private cipherAuthorizationService: CipherAuthorizationService, + private accountService: AccountService, ) {} /** @@ -103,8 +106,17 @@ export class ViewComponent implements OnInit { this.cipher = this.params.cipher; this.collections = this.params.collections; this.cipherTypeString = this.getCipherViewTypeString(); + + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (this.cipher.organizationId) { - this.organization = await this.organizationService.get(this.cipher.organizationId); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => organizations.find((o) => o.id === this.cipher.organizationId)), + ), + ); } this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index c8badffb36f5..37136a86cdba 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -6,7 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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"; diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts index 0fc7b6a31aa7..4058c1151fb4 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -10,11 +10,15 @@ import { OrganizationUserApiService, CollectionView, } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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 { 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 { GroupApiService, GroupView } from "../../../admin-console/organizations/core"; import { @@ -63,14 +67,22 @@ export class BulkCollectionsDialogComponent implements OnDestroy { private dialogRef: DialogRef, private formBuilder: FormBuilder, private organizationService: OrganizationService, + private accountService: AccountService, private groupService: GroupApiService, private organizationUserApiService: OrganizationUserApiService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private collectionAdminService: CollectionAdminService, + private toastService: ToastService, ) { this.numCollections = this.params.collections.length; - const organization$ = this.organizationService.get$(this.params.organizationId); + const organization$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe(getOrganizationById(this.params.organizationId)), + ), + ); const groups$ = organization$.pipe( switchMap((organization) => { if (!organization.useGroups) { @@ -119,7 +131,11 @@ export class BulkCollectionsDialogComponent implements OnDestroy { groups, ); - this.platformUtilsService.showToast("success", null, this.i18nService.t("editedCollections")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("editedCollections"), + }); this.dialogRef.close(BulkCollectionsDialogResult.Saved); }; diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts index 25976c4fb82c..5ccddeee4bb9 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -7,7 +7,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; @@ -50,8 +51,7 @@ describe("AdminConsoleCipherFormConfigService", () => { readOnly: false, } as CollectionAdminView; - const organization$ = new BehaviorSubject(testOrg as Organization); - const organizations$ = new BehaviorSubject([testOrg, testOrg2] as Organization[]); + const orgs$ = new BehaviorSubject([testOrg, testOrg2] as Organization[]); const getCipherAdmin = jest.fn().mockResolvedValue(null); const getCipher = jest.fn().mockResolvedValue(null); @@ -65,7 +65,7 @@ describe("AdminConsoleCipherFormConfigService", () => { TestBed.configureTestingModule({ providers: [ AdminConsoleCipherFormConfigService, - { provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } }, + { provide: OrganizationService, useValue: { organizations$: () => orgs$ } }, { provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([collection, collection2]) }, @@ -80,6 +80,17 @@ describe("AdminConsoleCipherFormConfigService", () => { }, { provide: ApiService, useValue: { getCipherAdmin } }, { provide: CipherService, useValue: { get: getCipher } }, + { + provide: AccountService, + useValue: { + activeAccount$: new BehaviorSubject({ + id: "123-456-789" as UserId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }), + }, + }, ], }); adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts index 0d3db55d3d61..32e75644d09b 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -9,17 +9,14 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherFormConfig, CipherFormConfigService, CipherFormMode } from "@bitwarden/vault"; -import { - CipherFormConfig, - CipherFormConfigService, - CipherFormMode, -} from "../../../../../../../libs/vault/src/cipher-form/abstractions/cipher-form-config.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; /** Admin Console implementation of the `CipherFormConfigService`. */ @@ -31,6 +28,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); private cipherService: CipherService = inject(CipherService); private apiService: ApiService = inject(ApiService); + private accountService: AccountService = inject(AccountService); private allowPersonalOwnership$ = this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -41,12 +39,16 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ filter((filter) => filter !== undefined), ); - private allOrganizations$ = this.organizationService.organizations$.pipe( - map((orgs) => { - return orgs.filter( - (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, - ); - }), + private allOrganizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService.organizations$(account?.id).pipe( + map((orgs) => { + return orgs.filter( + (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, + ); + }), + ), + ), ); private organization$ = combineLatest([this.allOrganizations$, this.organizationId$]).pipe( diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts index 0a8a6c9da94e..7e08af7c7f70 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts @@ -10,7 +10,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { VaultFilterComponent as BaseVaultFilterComponent } from "../../individual-vault/vault-filter/components/vault-filter.component"; //../../vault/vault-filter/components/vault-filter.component"; import { VaultFilterService } from "../../individual-vault/vault-filter/services/abstractions/vault-filter.service"; @@ -43,6 +43,7 @@ export class VaultFilterComponent protected policyService: PolicyService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected toastService: ToastService, protected billingApiService: BillingApiServiceAbstraction, protected dialogService: DialogService, protected configService: ConfigService, @@ -52,6 +53,7 @@ export class VaultFilterComponent policyService, i18nService, platformUtilsService, + toastService, billingApiService, dialogService, configService, diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html index 8178e3b29357..edba6e4753c3 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html @@ -104,8 +104,8 @@ *ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned && organization" class="tw-shrink-0" > - - + +

- - - -
- - - - - -
- - - - - - -
diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index d28148c49dc1..e3c99231a869 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -11,10 +11,8 @@ import { Unassigned, } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { CipherType } from "@bitwarden/common/vault/enums"; @@ -90,18 +88,11 @@ export class VaultHeaderComponent implements OnInit { @Output() searchTextChanged = new EventEmitter(); protected CollectionDialogTabType = CollectionDialogTabType; - protected organizations$ = this.organizationService.organizations$; - - /** - * Whether the extension refresh feature flag is enabled. - */ - protected extensionRefreshEnabled = false; /** The cipher type enum. */ protected CipherType = CipherType; constructor( - private organizationService: OrganizationService, private i18nService: I18nService, private dialogService: DialogService, private collectionAdminService: CollectionAdminService, @@ -109,11 +100,7 @@ export class VaultHeaderComponent implements OnInit { private configService: ConfigService, ) {} - async ngOnInit() { - this.extensionRefreshEnabled = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); - } + async ngOnInit() {} get title() { const headerType = this.i18nService.t("collections").toLowerCase(); diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 14550968ba5e..b4ba9ff55124 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -1,15 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef } from "@angular/cdk/dialog"; -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, - ViewChild, - ViewContainerRef, -} from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; import { BehaviorSubject, @@ -37,18 +29,17 @@ import { import { CollectionAdminService, CollectionAdminView, - CollectionService, CollectionView, Unassigned, } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; @@ -91,7 +82,7 @@ import { ResellerWarningService, } from "../../billing/services/reseller-warning.service"; import { TrialFlowService } from "../../billing/services/trial-flow.service"; -import { FreeTrial } from "../../core/types/free-trial"; +import { FreeTrial } from "../../billing/types/free-trial"; import { SharedModule } from "../../shared"; import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model"; @@ -126,7 +117,6 @@ import { import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.component"; import { getNestedCollectionTree } from "../utils/collection-utils"; -import { AddEditComponent } from "./add-edit.component"; import { BulkCollectionsDialogComponent, BulkCollectionsDialogResult, @@ -165,13 +155,6 @@ enum AddAccessStatusType { export class VaultComponent implements OnInit, OnDestroy { protected Unassigned = Unassigned; - @ViewChild("attachments", { read: ViewContainerRef, static: true }) - attachmentsModalRef: ViewContainerRef; - @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) - cipherAddEditModalRef: ViewContainerRef; - @ViewChild("collectionsModal", { read: ViewContainerRef, static: true }) - collectionsModalRef: ViewContainerRef; - trashCleanupWarning: string = null; activeFilter: VaultFilter = new VaultFilter(); @@ -209,23 +192,27 @@ export class VaultComponent implements OnInit, OnDestroy { private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); protected addAccessStatus$ = new BehaviorSubject(0); - private extensionRefreshEnabled: boolean; private resellerManagedOrgAlert: boolean; private vaultItemDialogRef?: DialogRef | undefined; - private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe( - filter((organizations) => organizations.length === 1), - map(([organization]) => organization), - switchMap((organization) => - from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( - tap((organizationMetaData) => { - this.hasSubscription$.next(organizationMetaData.hasSubscription); - }), - switchMap((organizationMetaData) => - from( - this.trialFlowService.handleUnpaidSubscriptionDialog( - organization, - organizationMetaData, + private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + switchMap((id) => + this.organizationService.organizations$(id).pipe( + filter((organizations) => organizations.length === 1), + map(([organization]) => organization), + switchMap((organization) => + from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( + tap((organizationMetaData) => { + this.hasSubscription$.next(organizationMetaData.hasSubscription); + }), + switchMap((organizationMetaData) => + from( + this.trialFlowService.handleUnpaidSubscriptionDialog( + organization, + organizationMetaData, + ), + ), ), ), ), @@ -243,7 +230,6 @@ export class VaultComponent implements OnInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, private syncService: SyncService, private i18nService: I18nService, - private modalService: ModalService, private dialogService: DialogService, private messagingService: MessagingService, private broadcasterService: BroadcasterService, @@ -259,7 +245,6 @@ export class VaultComponent implements OnInit, OnDestroy { private eventCollectionService: EventCollectionService, private totpService: TotpService, private apiService: ApiService, - private collectionService: CollectionService, private toastService: ToastService, private configService: ConfigService, private cipherFormConfigService: CipherFormConfigService, @@ -268,13 +253,10 @@ export class VaultComponent implements OnInit, OnDestroy { protected billingApiService: BillingApiServiceAbstraction, private organizationBillingService: OrganizationBillingServiceAbstraction, private resellerWarningService: ResellerWarningService, + private accountService: AccountService, ) {} async ngOnInit() { - this.extensionRefreshEnabled = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); - this.resellerManagedOrgAlert = await this.configService.getFeatureFlag( FeatureFlag.ResellerManagedOrgAlert, ); @@ -292,10 +274,19 @@ export class VaultComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - const organization$ = organizationId$.pipe( - switchMap((organizationId) => this.organizationService.get$(organizationId)), - takeUntil(this.destroy$), - shareReplay({ refCount: false, bufferSize: 1 }), + const organization$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + switchMap((id) => + organizationId$.pipe( + switchMap((organizationId) => + this.organizationService + .organizations$(id) + .pipe(map((organizations) => organizations.find((org) => org.id === organizationId))), + ), + takeUntil(this.destroy$), + shareReplay({ refCount: false, bufferSize: 1 }), + ), + ), ); const firstSetup$ = combineLatest([organization$, this.route.queryParams]).pipe( @@ -539,7 +530,7 @@ export class VaultComponent implements OnInit, OnDestroy { firstSetup$ .pipe( switchMap(() => combineLatest([this.route.queryParams, allCipherMap$])), - filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled), + filter(() => this.vaultItemDialogRef == undefined), switchMap(async ([qParams, allCiphersMap]) => { const cipherId = getCipherIdFromParams(qParams); @@ -570,15 +561,15 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - // Default to "view" if extension refresh is enabled - if (action == null && this.extensionRefreshEnabled) { + // Default to "view" + if (action == null) { action = "view"; } if (action === "view") { await this.viewCipherById(cipher); } else { - await this.editCipherId(cipher, false); + await this.editCipher(cipher, false); } } else { this.toastService.showToast({ @@ -820,27 +811,8 @@ export class VaultComponent implements OnInit, OnDestroy { } } + /** Opens the Add/Edit Dialog */ async addCipher(cipherType?: CipherType) { - if (this.extensionRefreshEnabled) { - return this.addCipherV2(cipherType); - } - - let collections: CollectionView[] = []; - - // Admins limited to only adding items to collections they have access to. - collections = await firstValueFrom(this.editableCollections$); - - await this.editCipher(null, false, (comp) => { - comp.type = cipherType || this.activeFilter.cipherType; - comp.collections = collections; - if (this.activeFilter.collectionId) { - comp.collectionIds = [this.activeFilter.collectionId]; - } - }); - } - - /** Opens the Add/Edit Dialog. Only to be used when the BrowserExtension feature flag is active */ - async addCipherV2(cipherType?: CipherType) { const cipherFormConfig = await this.cipherFormConfigService.buildConfig( "add", null, @@ -861,24 +833,8 @@ export class VaultComponent implements OnInit, OnDestroy { * Edit the given cipher or add a new cipher * @param cipherView - When set, the cipher to be edited * @param cloneCipher - `true` when the cipher should be cloned. - * Used in place of the `additionalComponentParameters`, as - * the `editCipherIdV2` method has a differing implementation. - * @param defaultComponentParameters - A method that takes in an instance of - * the `AddEditComponent` to edit methods directly. */ - async editCipher( - cipher: CipherView | null, - cloneCipher: boolean, - additionalComponentParameters?: (comp: AddEditComponent) => void, - ) { - return this.editCipherId(cipher, cloneCipher, additionalComponentParameters); - } - - async editCipherId( - cipher: CipherView | null, - cloneCipher: boolean, - additionalComponentParameters?: (comp: AddEditComponent) => void, - ) { + async editCipher(cipher: CipherView | null, cloneCipher: boolean) { if ( cipher && cipher.reprompt !== 0 && @@ -889,55 +845,6 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - if (this.extensionRefreshEnabled) { - await this.editCipherIdV2(cipher, cloneCipher); - return; - } - - const defaultComponentParameters = (comp: AddEditComponent) => { - comp.organization = this.organization; - comp.organizationId = this.organization.id; - comp.cipherId = cipher?.id; - comp.collectionId = this.activeFilter.collectionId; - comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - comp.onDeletedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - comp.onRestoredCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - }; - - const [modal, childComponent] = await this.modalService.openViewRef( - AddEditComponent, - this.cipherAddEditModalRef, - additionalComponentParameters == null - ? defaultComponentParameters - : (comp) => { - defaultComponentParameters(comp); - additionalComponentParameters(comp); - }, - ); - - // 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 - modal.onClosedPromise().then(() => { - this.go({ cipherId: null, itemId: null, action: null }); - }); - - return childComponent; - } - - /** - * Edit a cipher using the new AddEditCipherDialogV2 component. - * Only to be used behind the ExtensionRefresh feature flag. - */ - private async editCipherIdV2(cipher: CipherView | null, cloneCipher: boolean) { const cipherFormConfig = await this.cipherFormConfigService.buildConfig( cloneCipher ? "clone" : "edit", cipher?.id as CipherId | null, @@ -994,6 +901,7 @@ export class VaultComponent implements OnInit, OnDestroy { disableForm, activeCollectionId, isAdminConsoleAction: true, + restore: this.restore, }); const result = await lastValueFrom(this.vaultItemDialogRef.closed); @@ -1021,19 +929,10 @@ export class VaultComponent implements OnInit, OnDestroy { } } - let collections: CollectionView[] = []; - - // Admins limited to only adding items to collections they have access to. - collections = await firstValueFrom(this.editableCollections$); - - await this.editCipher(cipher, true, (comp) => { - comp.cloneMode = true; - comp.collections = collections; - comp.collectionIds = cipher.collectionIds; - }); + await this.editCipher(cipher, true); } - async restore(c: CipherView): Promise { + restore = async (c: CipherView): Promise => { if (!c.isDeleted) { return; } @@ -1064,7 +963,7 @@ export class VaultComponent implements OnInit, OnDestroy { } catch (e) { this.logService.error(e); } - } + }; async bulkRestore(ciphers: CipherView[]) { if ( diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.ts b/apps/web/src/app/vault/services/web-view-password-history.service.ts index 756c2140ab56..b1451b268de7 100644 --- a/apps/web/src/app/vault/services/web-view-password-history.service.ts +++ b/apps/web/src/app/vault/services/web-view-password-history.service.ts @@ -1,9 +1,9 @@ import { Injectable } from "@angular/core"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; -import { ViewPasswordHistoryService } from "../../../../../../libs/common/src/vault/abstractions/view-password-history.service"; import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; /** diff --git a/apps/web/src/app/vault/settings/purge-vault.component.ts b/apps/web/src/app/vault/settings/purge-vault.component.ts index 80b0448a39c1..83955c0dc949 100644 --- a/apps/web/src/app/vault/settings/purge-vault.component.ts +++ b/apps/web/src/app/vault/settings/purge-vault.component.ts @@ -11,7 +11,7 @@ import { Verification } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; export interface PurgeVaultDialogData { organizationId: string; @@ -37,6 +37,7 @@ export class PurgeVaultComponent { private userVerificationService: UserVerificationService, private router: Router, private syncService: SyncService, + private toastService: ToastService, ) { this.organizationId = data && data.organizationId ? data.organizationId : null; } @@ -46,7 +47,11 @@ export class PurgeVaultComponent { .buildRequest(this.formGroup.value.masterPassword) .then((request) => this.apiService.postPurgeCiphers(request, this.organizationId)); await response; - this.platformUtilsService.showToast("success", null, this.i18nService.t("vaultPurged")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("vaultPurged"), + }); await this.syncService.fullSync(true); if (this.organizationId != null) { await this.router.navigate(["organizations", this.organizationId, "vault"]); diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 1dd2d62f2a5e..6addd5408015 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Welke tipe item is dit?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Wysig vouer" }, + "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Basisdomein", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "bv.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Dien in" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "’n Kennisgewing is na u toestel gestuur." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Weergawe $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Hergenereer wagwoord" - }, "length": { "message": "Lengte" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Gevaarsone" }, - "dangerZoneDesc": { - "message": "Versigtig, hierdie aksies is onomkeerbaar!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Bestuur" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Deaktiveer" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "U herstelkode vir Bitwarden-tweestapaantekening" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Stel minimum vereistes vir opstelling van wagwoordgenereerder." }, - "passwordGeneratorPolicyInEffect": { - "message": "Een of meer organisasiebeleide beïnvloed u genereerderinstellings." - }, "masterPasswordPolicyInEffect": { "message": "Een of meer organisasiebeleide stel die volgende eise aan u hoofwagwoord:" }, @@ -6554,15 +6717,6 @@ "message": "Genereerder", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Wat wil u genereer?" - }, - "passwordType": { - "message": "Wagwoordtipe" - }, - "regenerateUsername": { - "message": "Hergenereer gebruikersnaam" - }, "generateUsername": { "message": "Genereer gebruikersnaam" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Gebruikersnaamtipe" - }, "plusAddressedEmail": { "message": "E-posadres met plus", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Gebruik u domein se opgestelde allesomvattende inmandjie." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Lukraak", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Gasheernaam", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API-toegangsteken" - }, "deviceVerification": { "message": "Toestelbevestiging" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Geen versameling" }, - "canView": { - "message": "Kan bekyk" - }, - "canViewExceptPass": { - "message": "Kan bekyk, behalwe wagwoorde" - }, - "canEdit": { - "message": "Kan wysig" - }, - "canEditExceptPass": { - "message": "Kan wysig behalwe wagwoorde" - }, "noCollectionsAdded": { "message": "Geen versamelings toegevoeg" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "maand per lid" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 2c2549e2f1ee..dc8fc7a900f0 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "ما هو نوع العنصر؟" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "النطاق الأساسي", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "مثال.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "بَدْء تسجيل الدخول" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "قدِّم" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "تم إرسال إشعار إلى جهازك." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "الإصدار $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "إعادة توليد كلمة المرور" - }, "length": { "message": "الطول" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "منطقة خطرة" }, - "dangerZoneDesc": { - "message": "احذر، لن تستطيع التراجع عن هذه الإجراءات!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "إدارة" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "المولّد", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "ما الذي ترغب في توليده؟" - }, - "passwordType": { - "message": "نوع كلمة المرور" - }, - "regenerateUsername": { - "message": "إعادة توليد اسم المستخدم" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "اسم المضيف", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index b140ae320bea..4c49936c88eb 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Riskli üzvlər" }, + "atRiskMembersWithCount": { + "message": "Risk altındakı üzvlər ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Risk altındakı tətbiqlər ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Bu üzvlər, tətbiqlərə zəif, ifşa olunmuş və ya təkrar istifadə olunan parollarla giriş edir." + }, + "atRiskApplicationsDescription": { + "message": "Bu tətbiqlər zəif, ifşa olunmuş və ya təkrar istifadə edilmiş parollara sahibdir." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Bu üzvlər, $APPNAME$ tətbiqinə zəif, ifşa olunmuş və ya təkrar istifadə olunan parollarla giriş edir.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Cəmi üzv" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Cəmi tətbiq" }, + "unmarkAsCriticalApp": { + "message": "Kritik tətbiq kimi işarəni götür" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritik tətbiq işarəsi uğurla götürüldü" + }, "whatTypeOfItem": { "message": "Bu elementin növü nədir?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Qovluğa düzəliş et" }, + "newFolder": { + "message": "Yeni qovluq" + }, + "folderName": { + "message": "Qovluq adı" + }, + "folderHintText": { + "message": "Ana qovluğun adından sonra \"/\" əlavə edərək qovluğu ardıcıl yerləşdirin. Nümunə: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Bu qovluğu həmişəlik silmək istədiyinizə əminsiniz?" + }, "baseDomain": { "message": "Baza domeni", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Element adı" }, - "cannotRemoveViewOnlyCollections": { - "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "məs.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Kimliyinizi doğrulayın" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." + }, + "continueLoggingIn": { + "message": "Giriş etməyə davam" + }, "whatIsADevice": { "message": "Cihaz nədir?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Giriş etmə başladıldı" }, + "logInRequestSent": { + "message": "Tələb göndərildi" + }, "submit": { "message": "Göndər" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Cihazınıza bir bildiriş göndərildi." }, + "notificationSentDevicePart1": { + "message": "Cihazınızda Bitwarden kilidini açın, ya da " + }, + "areYouTryingToAccessYourAccount": { + "message": "Hesabınıza müraciət etməyə çalışırsınız?" + }, + "accessAttemptBy": { + "message": "$EMAIL$ ilə müraciət cəhdi", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Müraciəti təsdiqlə" + }, + "denyAccess": { + "message": "Müraciətə rədd cavabı ver" + }, + "notificationSentDeviceAnchor": { + "message": "veb tətbiqinizdə" + }, + "notificationSentDevicePart2": { + "message": "Təsdiqləməzdən əvvəl Barmaq izi ifadəsinin aşağıdakı ifadə ilə uyuşduğuna əmin olun." + }, + "notificationSentDeviceComplete": { + "message": "Cihazınızda Bitwarden-in kilidini açın. Təsdiqləməzdən əvvəl Barmaq izi ifadəsinin aşağıdakı ifadə ilə uyuşduğuna əmin olun." + }, "aNotificationWasSentToYourDevice": { "message": "Cihazınıza bir bildiriş göndərildi" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Hesabınızın kilidinin açıq olduğuna və barmaq izi ifadəsinin digər cihazda uyuşduğuna əmin olun" - }, "versionNumber": { "message": "Versiya $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Anlaşılmaz xarakterlərdən çəkin", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Parolu yenidən yarat" - }, "length": { "message": "Uzunluq" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Təhlükəli zona" }, - "dangerZoneDesc": { - "message": "Diqqətli olun, bu əməliyyatları geri qaytara bilməzsiniz!" - }, - "dangerZoneDescSingular": { - "message": "Diqqətli olun, bu əməliyyatı geri qaytara bilməzsiniz!" - }, "deauthorizeSessions": { "message": "Seansların səlahiyyətlərini götür" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Davam etsəniz, hazırkı seansınız bitəcək, təkrar giriş etməyiniz tələb olunacaq. Fəallaşdırılıbsa, iki addımlı giriş üçün yenidən soruşulacaq. Digər cihazlardakı aktiv seanslar, bir saata qədər aktiv qalmağa davam edə bilər." }, + "newDeviceLoginProtection": { + "message": "Yeni cihaz girişi" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Yeni cihaz girişi qorumasını söndür" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Yeni cihaz girişi qorumasını işə sal" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Yeni bir cihazdan giriş etdiyiniz zaman Bitwarden göndərən doğrulama e-poçtlarını dayandırmaq üçün aşağıdakı addımları izləyin." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Yeni bir cihazdan giriş etdiyiniz zaman Bitwarden-in sizə doğrulama e-poçtlarını göndərməsi üçün aşağıdakı addımları izləyin." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Yeni cihaz girişi qorumasını söndürsəniz, ana parolunuzu bilən hər kəs, istənilən cihazdan hesabınıza müraciət edə bilər. Hesabınızı doğrulama e-poçtları olmadan qorumaq üçün iki addımlı girişi qurun." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Yeni cihaz girişi qoruması dəyişiklikləri saxlanıldı" + }, "sessionsDeauthorized": { "message": "Bütün seansların səlahiyyəti götürüldü" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "İdarə et" }, - "canManage": { - "message": "İdarə edə bilər" + "manageCollection": { + "message": "Kolleksiyanı idarə et" + }, + "viewItems": { + "message": "Elementlərə bax" + }, + "viewItemsHidePass": { + "message": "Elementlərə, gizli parollara bax" + }, + "editItems": { + "message": "Elementlərə düzəliş et" + }, + "editItemsHidePass": { + "message": "Elementlərə, gizli parollara düzəliş et" }, "disable": { "message": "Sıradan çıxart" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Güvənlik açarı oxunarkən problem yarandı. Yenidən sınayın." }, - "twoFactorWebAuthnWarning": { - "message": "Platforma məhdudiyyətlərinə görə, WebAuthn bütün Bitwarden tətbiqlərində istifadə edilə bilmir. WebAuthn istifadə edilə bilməyəndə, hesabınıza müraciət edə bilməyiniz üçün başqa bir iki addımlı giriş provayderini fəallaşdırmalısınız. Dəstəklənən platformalar:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "WebAuthn dəstəkli brauzerə sahib masaüstü/dizüstü kompüterdə veb seyf və brauzer uzantıları (FIDO U2F açıq olan Chrome, Opera, Vivaldi və ya Firefox)." + "twoFactorWebAuthnWarning1": { + "message": "Platforma məhdudiyyətlərinə görə, WebAuthn bütün Bitwarden tətbiqlərində istifadə edilə bilmir. WebAuthn istifadə edilə bilməyəndə, hesabınıza müraciət edə bilməyiniz üçün başqa bir iki addımlı giriş provayderini fəallaşdırmalısınız." }, "twoFactorRecoveryYourCode": { "message": "Bitwarden iki addımlı giriş üçün geri qaytarma kodunuz" @@ -2953,7 +3052,7 @@ "message": "Əlavə istifadəçi yerləri" }, "userSeatsDesc": { - "message": "İstifadəçi sayı" + "message": "# / istifadəçi yeri" }, "userSeatsAdditionalDesc": { "message": "Planınızda $BASE_SEATS$ istifadəçi yeri var. İstifadəçiləri, istifadəçi/ay başına $SEAT_PRICE$ qarşılığında əlavə edə bilərsiniz.", @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "1 dəvət haqqınız var." }, + "inviteZeroEmailDesc": { + "message": "0 dəvət haqqınız var." + }, "userUsingTwoStep": { "message": "Bu istifadəçinin hesabını qorumaq üçün iki addımlı giriş istifadə edilir." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Əlaqələndirilməmiş SSO." + }, "unlinkedSsoUser": { "message": "$ID$ istifadəçisi üçün SSO əlaqəsi kəsildi.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Güvənli" }, + "needsApproval": { + "message": "Təsdiq lazımdır" + }, + "areYouTryingtoLogin": { + "message": "Giriş etməyə çalışırsınız?" + }, + "logInAttemptBy": { + "message": "$EMAIL$ tərəfindən giriş cəhdi", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Cihaz növü" + }, + "ipAddress": { + "message": "IP Ünvan" + }, + "confirmLogIn": { + "message": "Girişi təsdiqlə" + }, + "denyLogIn": { + "message": "Girişə rədd cavabı ver" + }, + "thisRequestIsNoLongerValid": { + "message": "Bu tələb artıq yararsızdır." + }, + "logInConfirmedForEmailOnDevice": { + "message": "$DEVICE$ cihazında $EMAIL$ üçün giriş təsdiqləndi", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Başqa bir cihazdan giriş cəhdinə rədd cavabı verdiniz. Bu həqiqətən siz idinizsə, cihazla yenidən giriş etməyə çalışın." + }, + "loginRequestHasAlreadyExpired": { + "message": "Giriş tələbinin müddəti artıq bitib." + }, + "justNow": { + "message": "İndicə" + }, + "requestedXMinutesAgo": { + "message": "$MINUTES$ dəqiqə əvvəl tələb göndərildi", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Hesab yaradılır" }, @@ -4157,7 +4323,7 @@ "message": "Sirr Menecerinə abunəliyiniz üçün bir limit müəyyən edin. Bu limitə çatanda, yeni istifadəçiləri dəvət edə bilməyəcəksiniz." }, "maxSeatLimit": { - "message": "Maksimum yer limiti (ixtiyari)", + "message": "Yer limiti (ixtiyari)", "description": "Upper limit of seats to allow through autoscaling" }, "maxSeatCost": { @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Parol yaradıcı üçün tələbləri ayarla." }, - "passwordGeneratorPolicyInEffect": { - "message": "Bir və ya daha çox təşkilat siyasətləri yaradıcı seçimlərinizə təsir edir." - }, "masterPasswordPolicyInEffect": { "message": "Bir və ya daha çox təşkilat siyasəti, aşağıdakı tələbləri qarşılamaq üçün ana parolunuzu tələb edir:" }, @@ -6554,15 +6717,6 @@ "message": "Yaradıcı", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Nə yaratmaq istəyirsiniz?" - }, - "passwordType": { - "message": "Parol növü" - }, - "regenerateUsername": { - "message": "İstifadəçi adını yenidən yarat" - }, "generateUsername": { "message": "İstifadəçi adı yarat" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "İstifadəçi adı növü" - }, "plusAddressedEmail": { "message": "Plyus ünvanlı e-poçt", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Domeninizin konfiqurasiya edilmiş hamısını yaxalama gələn qutusunu istifadə edin." }, + "useThisEmail": { + "message": "Bu e-poçtu istifadə et" + }, "random": { "message": "Təsadüfi", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Host adı", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API müraciət tokeni" - }, "deviceVerification": { "message": "Cihaz doğrulaması" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Kolleksiya yoxdur" }, - "canView": { - "message": "Baxa bilər" - }, - "canViewExceptPass": { - "message": "Parollar istisna olmaqla baxa bilər" - }, - "canEdit": { - "message": "Düzəliş edə bilər" - }, - "canEditExceptPass": { - "message": "Parollar istisna olmaqla düzəliş edə bilər" - }, "noCollectionsAdded": { "message": "Heç bir kolleksiya əlavə edilmədi" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Güvənli cihazlar" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Kimlik doğrulandıqdan sonra üzvlər, cihazlarından saxlanılan açarı istifadə edərək seyf datasının şifrəsini aça biləcək", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "SSO ilə giriş edərkən üzvlərin ana parola ehtiyacı olmayacaq. Ana parol, cihazda saxlanılan bir şifrələmə açarı ilə əvəz olunur və bu da cihazı güvənli hala gətirir. Üzvün hesabını yaradıb giriş etdiyi ilk cihaz, güvənli hesab olunacaq. Yeni cihazların mövcud güvənli cihaz və ya bir inzibatçı tərəfindən təsdiqlənməsi lazım gələcək.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "vahid təşkilat", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "Vahid təşkilat", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "siyasəti,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO tələb olunur", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO tələb olunan", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "siyasəti və", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "siyasət və", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "hesab geri qaytarma administrasiyası", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "bu seçim istifadə edildikdə avto-yazılma ilə işə salınacaq.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "siyasəti, bu seçim istifadə olunduqda işə düşəcək.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Təşkilatınızın icazələri güncəlləndi və bir ana parol ayarlamağınızı tələb edir.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Kolleksiya silinməsini sahibləri və adminləri ilə məhdudlaşdır" }, + "limitItemDeletionDesc": { + "message": "Elementin silinməsini \"İdarə edə bilər\" icazəsinə sahib üzvlərlə məhdudlaşdır" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Sahiblər və adminlər bütün kolleksiyaları və elementləri idarə edə bilər" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL-si", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Domen ləqəbi" - }, "alreadyHaveAccount": { "message": "Artıq bir hesabınız var?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Bu kolleksiyanı idarə etmək üçün müraciətiniz yoxdur." }, - "grantAddAccessCollectionWarningTitle": { - "message": "İdarə edə bilər icazələri əskikdir" + "grantManageCollectionWarningTitle": { + "message": "\"Kolleksiyanı idarə etmə\" icazələri əskikdir" }, - "grantAddAccessCollectionWarning": { - "message": "Kolleksiyanın silinməsi daxil olmaqla tam kolleksiya idarəetməsinə icazə vermək üçün \"İdarə edə bilər\" icazələrini verin." + "grantManageCollectionWarning": { + "message": "Kolleksiyanın silinməsi daxil olmaqla tam kolleksiya idarəetməsinə icazə vermək üçün \"Kolleksiyanı idarə etmə\" icazələrini verin." }, "grantCollectionAccess": { "message": "Qrup və ya üzvlərin bu kolleksiyaya müraciətinə icazə verin." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "üzv başına ay" }, + "monthPerMemberBilledAnnually": { + "message": "üzv üzrə aylıq illik hesablama" + }, "seats": { "message": "Yer" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Açar alqoritmi" }, + "sshPrivateKey": { + "message": "Private açar" + }, + "sshPublicKey": { + "message": "Public açar" + }, + "sshFingerprint": { + "message": "Barmaq izi" + }, "sshKeyFingerprint": { "message": "Barmaq izi" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Açıqlayıcı kod" }, + "cannotRemoveViewOnlyCollections": { + "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Vacib bildiriş" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Təşkilat adı 50 xarakterdən çox ola bilməz." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Abunəliyiniz tezliklə yenilənəcək. Kəsintisiz xidməti təmin etmək və yeniləməni $RENEWAL_DATE$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Abunəliyinizə aid faktura $ISSUED_DATE$ tarixində təqdim edildi. Kəsintisiz xidməti təmin etmək və yeniləməni $DUE_DATE$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Abunəliyinizə aid faktura üzrə ödəniş edilmədi. Kəsintisiz xidməti təmin etmək və yeniləməni $GRACE_PERIOD_END$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "İnzibatçılar, artıq götürülmüş domenlərə aid üzv hesablarını silmə imkanına sahibdir." + }, + "deleteManagedUserWarningDesc": { + "message": "Bu əməliyyat, seyfdəki bütün elementlər daxil olmaqla üzv hesabını siləcək. Bu, əvvəlki Sil əməliyyatını əvəz edir." + }, + "deleteManagedUserWarning": { + "message": "Silmək, yeni bir əməliyyatdır!" + }, + "seatsRemaining": { + "message": "Bu təşkilata təyin edilmiş $TOTAL$ yerdən $REMAINING$ yeriniz qalıb. Abunəliyinizi idarə etmək üçün provayderinizlə əlaqə saxlayın.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Mövcud təşkilat" + }, + "selectOrganizationProviderPortal": { + "message": "Provayder Portalınıza əlavə ediləcək təşkilatı seçin." + }, + "noOrganizations": { + "message": "Sadalanacaq heç bir təşkilat yoxdur." + }, + "yourProviderSubscriptionCredit": { + "message": "Provayder abunəliyiniz, təşkilatınızın abunəliyində qalan vaxt üçün bir kredit alacaq." + }, + "doYouWantToAddThisOrg": { + "message": "Bu təşkilatı bura əlavə etmək istəyirsiniz: $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Mövcud təşkilat əlavə edildi" + }, + "assignedExceedsAvailable": { + "message": "Təyin edilmiş yer sayı, boş yer sayından çoxdur." } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 2a037c5b11cb..8dcb31a1d0f9 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -1,24 +1,24 @@ { "allApplications": { - "message": "All applications" + "message": "Усе праграмы" }, "criticalApplications": { - "message": "Critical applications" + "message": "Крытычныя праграмы" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "Кіраванне доступам" }, "riskInsights": { - "message": "Risk Insights" + "message": "Разуменне рызык" }, "passwordRisk": { - "message": "Password Risk" + "message": "Рызыка пароля" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Праглядайце паролі, якія знаходзяцца ў зоне рызыкі (ненадзейныя, скампраметаваныя або паўторна выкарыстаныя) ва ўсіх вашых праграмах. Выберыце найбольш крытычныя праграмы для вызначэння прыярытэту бяспекі дзеянняў для вашых карыстальнікаў, якія выкарыстоўваюць рызыкоўныя паролі." }, "dataLastUpdated": { - "message": "Data last updated: $DATE$", + "message": "Апошняе абнаўленне даных: $DATE$", "placeholders": { "date": { "content": "$1", @@ -27,19 +27,19 @@ } }, "notifiedMembers": { - "message": "Notified members" + "message": "Апавешчаныя ўдзельнікі" }, "revokeMembers": { - "message": "Revoke members" + "message": "Адклікаць удзельнікаў" }, "restoreMembers": { - "message": "Restore members" + "message": "Аднавіць удзельнікаў" }, "cannotRestoreAccessError": { - "message": "Cannot restore organization access" + "message": "Немагчыма аднавіць доступ да арганізацыі" }, "allApplicationsWithCount": { - "message": "All applications ($COUNT$)", + "message": "Усе праграмы ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -48,10 +48,10 @@ } }, "createNewLoginItem": { - "message": "Create new login item" + "message": "Стварыць новы элемент запісу ўваходу" }, "criticalApplicationsWithCount": { - "message": "Critical applications ($COUNT$)", + "message": "Крытычныя праграмы ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -60,7 +60,7 @@ } }, "notifiedMembersWithCount": { - "message": "Notified members ($COUNT$)", + "message": "Апавешчаныя ўдзельнікі ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -69,7 +69,7 @@ } }, "noAppsInOrgTitle": { - "message": "No applications found in $ORG NAME$", + "message": "Праграмы ў $ORG NAME$ не знойдзены", "placeholders": { "org name": { "content": "$1", @@ -78,49 +78,88 @@ } }, "noAppsInOrgDescription": { - "message": "As users save logins, applications appear here, showing any at-risk passwords. Mark critical apps and notify users to update passwords." + "message": "Тут будуць адлюстроўвацца праграмы па меры таго, як карыстальнікі будуць захоўваць запісы ўваходу, якія знаходзяцца ў зоне рызыкі. Пазначце крытычныя праграмы і апавяшчайце карыстальнікаў аб неабходнасці абнавіць паролі." }, "noCriticalAppsTitle": { - "message": "You haven't marked any applications as a Critical" + "message": "Вы не пазначылі ніводную праграму ў якасці кратычнай" }, "noCriticalAppsDescription": { - "message": "Select your most critical applications to discover at-risk passwords, and notify users to change those passwords." + "message": "Выберыце найбольш крытычныя праграмы для выяўлення пароляў, якія знаходзяцца ў зоне рызыкі. Апавяшчайце карыстальнікаў аб неабходнасці змяніць іх." }, "markCriticalApps": { - "message": "Mark critical apps" + "message": "Пазначыць крытычныя праграмы" }, "markAppAsCritical": { - "message": "Mark app as critical" + "message": "Пазначыць праграму як крытычную" }, "appsMarkedAsCritical": { - "message": "Apps marked as critical" + "message": "Праграмы пазначаныя як крытычныя" }, "application": { - "message": "Application" + "message": "Праграма" }, "atRiskPasswords": { - "message": "At-risk passwords" + "message": "Паролі ў зоне рызыкі" }, "requestPasswordChange": { - "message": "Request password change" + "message": "Запытаць змену пароля" }, "totalPasswords": { - "message": "Total passwords" + "message": "Усяго пароляў" }, "searchApps": { - "message": "Search applications" + "message": "Пошук праграм" }, "atRiskMembers": { - "message": "At-risk members" + "message": "Удзельнікі ў зоне рызыкі" + }, + "atRiskMembersWithCount": { + "message": "Удзельнікі ў зоне рызыкі ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Праграмы ў зоне рызыкі ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Гэтыя ўдзельнікі ўваходзяць у праграму з ненадзейнымі, скампраметаванымі або паўторна выкарыстанымі паролямі." + }, + "atRiskApplicationsDescription": { + "message": "Гэтыя праграмы маюць ненадзейныя, скампраметаваныя або паўторна выкарыстаныя паролі." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Гэтыя ўдзельнікі ўваходзяць у праграму $APPNAME$ з ненадзейнымі, скампраметаванымі або паўторна выкарыстанымі паролямі.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } }, "totalMembers": { - "message": "Total members" + "message": "Усяго ўдзельнікаў" }, "atRiskApplications": { - "message": "At-risk applications" + "message": "Праграмы ў зоне рызыкі" }, "totalApplications": { - "message": "Total applications" + "message": "Усяго праграм" + }, + "unmarkAsCriticalApp": { + "message": "Зняць пазнаку крытычнай праграмы" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Пазнака з крытычнай праграмы паспяхова знята" }, "whatTypeOfItem": { "message": "Які гэта элемент запісу?" @@ -221,10 +260,10 @@ "message": "Дадаць вэб-сайт" }, "deleteWebsite": { - "message": "Delete website" + "message": "Выдаліць вэб-сайт" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Прадвызначана ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -234,7 +273,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Паказаць выяўленне супадзенняў $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -243,7 +282,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Схаваць выяўленне супадзенняў $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -252,7 +291,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Аўтазапаўняць пры загрузцы старонкі?" }, "number": { "message": "Нумар" @@ -267,7 +306,7 @@ "message": "Код бяспекі (CVV)" }, "securityCodeSlashCVV": { - "message": "Security code / CVV" + "message": "Код бяспекі (CVV)" }, "identityName": { "message": "Імя пасведчання" @@ -345,10 +384,10 @@ "message": "Доктар" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Картка пратэрмінавана" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "Калі вы падаўжалі картку, то абнавіце яе звесткі" }, "expirationMonth": { "message": "Месяц завяршэння" @@ -360,7 +399,7 @@ "message": "Ключ аўтэнтыфікацыі (TOTP)" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "Спрасціце двухэтапную праверку" }, "totpHelper": { "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Асноўны дамен", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "напр.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Ініцыяваны ўваход" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Адправіць" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Апавяшчэнне было адпраўлена на вашу прыладу." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Версія $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Паўторна генерыраваць пароль" - }, "length": { "message": "Даўжыня" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Небяспечная зона" }, - "dangerZoneDesc": { - "message": "Асцярожна, гэтыя дзеянні з'яўляюцца незваротнымі!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Скасаваць аўтарызацыю сеанса" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Працягваючы, вы таксама выйдзіце з бягучага сеанса і вам неабходна будзе ўвайсці паўторна. Вы таксама атрымаеце паўторны запыт двухэтапнага ўваходу, калі гэта функцыя ў вас уключана. Сеансы на іншых прыладах могуць заставацца актыўнымі на працягу адной гадзіны." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Аўтарызацыя ўсіх сеансаў скасавана" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Кіраванне" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Адключыць" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Праблема чытання ключа бяспекі. Паспрабуйце яшчэ раз." }, - "twoFactorWebAuthnWarning": { - "message": "У сувязі з абмежаваннямі платформы, WebAuthn немагчыма выкарыстоўваць ва ўсіх праграмах Bitwarden. Вам неабходна актываваць іншага пастаўшчыка двухэтапнага ўваходу, каб вы маглі атрымаць доступ да свайго ўліковага запісу, калі немагчыма скарыстацца WebAuthn. Платформы, які падтрымліваюцца:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Вэб-сховішча і пашырэнні браўзера на камп'ютары/ноўтбуку з браўзерам, які падтрымлівае WebAuthn (Chrome, Opera, Vivaldi або Firefox з уключаным FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Ваш код аднаўлення двухэтапнага ўваходу Bitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Гэты карыстальнік выкарыстоўвае двухэтапны ўваход для абароны свайго ўліковага запісу." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Адлучаныя SSO для карыстальніка $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Прызначыць патрабаванні для генератара пароляў." }, - "passwordGeneratorPolicyInEffect": { - "message": "Адна або больш палітык арганізацыі ўплывае на налады генератара." - }, "masterPasswordPolicyInEffect": { "message": "Адна або больш палітык арганізацыі патрабуе, каб ваш асноўны пароль адпавядаў наступным патрабаванням:" }, @@ -6554,15 +6717,6 @@ "message": "Генератар", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Што вы хочаце генерыраваць?" - }, - "passwordType": { - "message": "Тып пароля" - }, - "regenerateUsername": { - "message": "Паўторна генерыраваць імя карыстальніка" - }, "generateUsername": { "message": "Генерыраваць імя карыстальніка" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Тып імя карыстальніка" - }, "plusAddressedEmail": { "message": "Адрасы электроннай пошты з плюсам", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Выкарыстоўвайце сваю сканфігураваную скрыню для ўсёй пошты дамена." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Выпадкова", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Назва вузла", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Токен доступу да API" - }, "deviceVerification": { "message": "Праверка прылады" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Няма калекцый" }, - "canView": { - "message": "Можа праглядаць" - }, - "canViewExceptPass": { - "message": "Можа праглядаць (без пароляў)" - }, - "canEdit": { - "message": "Можа рэдагаваць" - }, - "canEditExceptPass": { - "message": "Можа рэдагаваць (без пароляў)" - }, "noCollectionsAdded": { "message": "Няма дадзеных калекцый" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Давераныя прылады" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Пасля аўтэнтыфікацыі ўдзельнікі расшыфроўваюць даныя сховішча з выкарыстаннем ключа, які захоўваецца на іх прыладах.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "адзіная арганізацыя", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "палітыка", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "патрабуецца SSO", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "палітыка і", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "адміністраванне аднаўлення ўліковага запісу", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "палітыка з аўтаматычнай рэгістрацыяй уключаецца пры выкарыстанні гэтага параметра.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Мянушка дамена" - }, "alreadyHaveAccount": { "message": "Ужо маеце ўліковы запіс?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 531cfd18a42c..c985e4eb74e6 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -15,7 +15,7 @@ "message": "Рискова парола" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Прегледайте паролите в риск (слаби, преизползвани или разобличени) в различните приложения. Изберете най-важните си приложения, за да дадете приоритет на действията по сигурността за потребителите си, така че те да обърнат внимание на паролите си в риск." }, "dataLastUpdated": { "message": "Последно обновяване на данните: $DATE$", @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Членове в риск" }, + "atRiskMembersWithCount": { + "message": "Членове в риск ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Приложения в риск ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Тези членове се вписват в приложенията със слаби, преизползвани или разобличени пароли." + }, + "atRiskApplicationsDescription": { + "message": "Тези приложения имат слаби, преизползвани или разобличени пароли." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Тези членове се вписват в $APPNAME$ със слаби, преизползвани или разобличени пароли.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Общо членове" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Общо приложения" }, + "unmarkAsCriticalApp": { + "message": "Премахване на приложението от важните" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Приложението е премахнато от важните" + }, "whatTypeOfItem": { "message": "Вид на елемента" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Редактиране на папка" }, + "newFolder": { + "message": "Нова папка" + }, + "folderName": { + "message": "Име на папката" + }, + "folderHintText": { + "message": "Можете да вложите една папка в друга като въведете името на горната папка, а след това „/“. Пример: Социални/Форуми" + }, + "deleteFolderPermanently": { + "message": "Наистина ли искате да изтриете тази папка окончателно?" + }, "baseDomain": { "message": "Основен домейн", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Име на елемента" }, - "cannotRemoveViewOnlyCollections": { - "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "напр.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Потвърдете самоличността си" }, + "weDontRecognizeThisDevice": { + "message": "Това устройство е непознато. Въведете кода изпратен на е-пощата Ви, за да потвърдите самоличността си." + }, + "continueLoggingIn": { + "message": "Продължаване с вписването" + }, "whatIsADevice": { "message": "Какво представлява едно устройство?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Вписването е стартирано" }, + "logInRequestSent": { + "message": "Заявката е изпратена" + }, "submit": { "message": "Подаване" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Към устройството Ви е изпратено известие." }, + "notificationSentDevicePart1": { + "message": "Отключете Битоурден на устройството си или в " + }, + "areYouTryingToAccessYourAccount": { + "message": "Опитвате ли се да получите достъп до акаунта си?" + }, + "accessAttemptBy": { + "message": "Опит за достъп от $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Разрешаване на достъпа" + }, + "denyAccess": { + "message": "Отказване на достъпа" + }, + "notificationSentDeviceAnchor": { + "message": "приложението по уеб" + }, + "notificationSentDevicePart2": { + "message": "Уверете се, че уникалната фраза съвпада с тази по-долу, преди да одобрите." + }, + "notificationSentDeviceComplete": { + "message": "Отключете Битоурден на устройството си. Уверете се, че уникалната фраза съвпада с тази по-долу, преди да одобрите." + }, "aNotificationWasSentToYourDevice": { "message": "Към устройството Ви е изпратено известие" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Уверете се, че регистрацията Ви е отключена и че уникалната фраза съвпада с другото устройство" - }, "versionNumber": { "message": "Версия $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Без нееднозначни знаци", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Друга парола" - }, "length": { "message": "Дължина" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "ОПАСНО" }, - "dangerZoneDesc": { - "message": "Внимание, тези действия са необратими!" - }, - "dangerZoneDescSingular": { - "message": "Внимание, това действие е необратимо!" - }, "deauthorizeSessions": { "message": "Прекратяване на сесии" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Действието ще прекрати и текущата ви сесия, след което ще се наложи отново да се впишете. Ако сте включили двустепенна идентификация, ще се наложи да повторите и нея. Активните сесии на другите устройства може да останат такива до един час." }, + "newDeviceLoginProtection": { + "message": "Вписване от ново устройство" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Изключване на защитата за вписване от ново устройство" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Включване на защитата за вписване от ново устройство" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Продължете по-долу, за да изключите е-писмата за потвърждение, които Битуорден изпраща, когато се вписвате от ново устройство." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Продължете по-долу, за да посочите, че искате Биуорден да изпраща е-писма за потвърждение, когато се вписвате от ново устройство." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Ако защитата за вписване от ново устройство е изключена, всеки, който знае главната Ви парола, ще може да получи достъп до акаунта Ви от всяко устройство. Ако искате да защитите акаунта си без потвърждение чрез е-поща, настройте двустепенното удостоверяване." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Промените на защитата за вписване от ново устройство са запазени" + }, "sessionsDeauthorized": { "message": "Всички сесии са прекратени" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Управление" }, - "canManage": { - "message": "Може да управлява" + "manageCollection": { + "message": "Управление на колекцията" + }, + "viewItems": { + "message": "Преглед на елементите" + }, + "viewItemsHidePass": { + "message": "Преглед на елементите, със скрити пароли" + }, + "editItems": { + "message": "Редактиране на елементите" + }, + "editItemsHidePass": { + "message": "Редактиране на елементите, със скрити пароли" }, "disable": { "message": "Изключване" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Проблем при изчитането на ключа за сигурност. Пробвайте отново." }, - "twoFactorWebAuthnWarning": { - "message": "Поради платформени ограничения устройствата на WebAuthn не могат да се използват с всички приложения на Битуорден. В такъв случай ще трябва да добавите друг доставчик на двустепенно удостоверяване, за да имате достъп до абонамента си, дори когато WebAuthn не работи. Поддържани платформи:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Трезорът по уеб както и разширенията за браузърите с поддръжка на WebAuthn (Chrome, Opera, Vivaldi и Firefox с поддръжка на FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Поради платформени ограничения устройствата на WebAuthn не могат да се използват с всички приложения на Битуорден. Ще трябва да настроите друг доставчик на двустепенно удостоверяване, за да имате достъп до акаунта си, когато WebAuthn не може да се ползва." }, "twoFactorRecoveryYourCode": { "message": "Код за възстановяване на достъпа до Битуорден при двустепенна идентификация" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Имате 1 оставаща покана." }, + "inviteZeroEmailDesc": { + "message": "Имате 0 оставащи покани." + }, "userUsingTwoStep": { "message": "Този потребител използва двустепенна защита за достъп." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Еднократната идентификация е прекъсната." + }, "unlinkedSsoUser": { "message": "Самоличността $ID$ е извадено от еднократното вписване.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Доверено" }, + "needsApproval": { + "message": "Изисква одобрение" + }, + "areYouTryingtoLogin": { + "message": "Опитвате се да се впишете ли?" + }, + "logInAttemptBy": { + "message": "Опит за вписване от $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Вид устройство" + }, + "ipAddress": { + "message": "IP адрес" + }, + "confirmLogIn": { + "message": "Потвърждаване на вписването" + }, + "denyLogIn": { + "message": "Отказване на вписването" + }, + "thisRequestIsNoLongerValid": { + "message": "Тази заявка вече не е активна." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Вписването за $EMAIL$ на $DEVICE$ е одобрено", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Вие отказахте опит за вписване от друго устройство. Ако това наистина сте били Вие, опитайте да се впишете от устройството отново." + }, + "loginRequestHasAlreadyExpired": { + "message": "Заявката за вписване вече е изтекла." + }, + "justNow": { + "message": "Току-що" + }, + "requestedXMinutesAgo": { + "message": "Заявено преди $MINUTES$ минути", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Създаване на регистрация в" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Задаване на минимална сила на генератора на пароли." }, - "passwordGeneratorPolicyInEffect": { - "message": "Поне една политика на организация влияе на настройките на генерирането на паролите." - }, "masterPasswordPolicyInEffect": { "message": "Поне една политика на организация има следните изисквания към главната ви парола:" }, @@ -6554,15 +6717,6 @@ "message": "Генератор", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Какво бихте искали да генерирате?" - }, - "passwordType": { - "message": "Тип парола" - }, - "regenerateUsername": { - "message": "Повторно генериране на потр. име" - }, "generateUsername": { "message": "Генериране на потр. име" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Тип потребителско име" - }, "plusAddressedEmail": { "message": "Адрес на е-поща с плюс", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Използване на тази е-поща" + }, "random": { "message": "Произволно", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Име на сървъра", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Идентификатор за достъп до API" - }, "deviceVerification": { "message": "Потвърждаване на устройствата" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Няма колекция" }, - "canView": { - "message": "Може да преглежда" - }, - "canViewExceptPass": { - "message": "Може да преглежда, без пароли" - }, - "canEdit": { - "message": "Може да редактира" - }, - "canEditExceptPass": { - "message": "Може да редактира, без пароли" - }, "noCollectionsAdded": { "message": "Няма добавени колекции" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Доверени устройства" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "След вписване, членовете ще дешифрират данните от трезорите си чрез ключ, който се съхранява на устройство. Политиката за", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Членовете няма да имат нужда от главна парола, когато се вписват чрез еднократно удостоверяване. Главната парола се заменя от шифроващ ключ съхраняван на устройството, което прави това устройство доверено. Първото устройство, с което членът създаде акаунта си и на което се впише, ще бъде доверено. Новите устройства ще трябва да бъдат одобрени от съществуващо доверено устройство или от администратор. Политиката за", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "единствена организация", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": ",", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": ", политиката за", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "политиката за изискване на еднократно удостоверяване", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "еднократно удостоверяване", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "и", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "и политиката за", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "политиката за управление на възстановяването на регистрации", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "администриране на възстановяването на акаунти", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "с автоматично включване ще бъдат активирани при използването на тази настройка.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "ще бъдат включени, когато се ползва тази опция.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Правата Ви в организацията бяха променени, необходимо е да зададете главна парола.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Ограничаване на изтриването на колекции, така че да може да се извършва само от собствениците и администраторите" }, + "limitItemDeletionDesc": { + "message": "Ограничаване на изтриването на елементи, така че да може да се извършва само от членове с правомощие за управление" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Собствениците и администраторите могат да управляват всички колекции и елементи" }, @@ -8544,9 +8686,6 @@ "message": "Адрес на собствения сървър", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Псевдонимен домейн" - }, "alreadyHaveAccount": { "message": "Вече имате регистрация?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Нямате достъп за управление на тази колекция." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Липсват правомощия за управление" + "grantManageCollectionWarningTitle": { + "message": "Липсват правомощия за управление на колекции" }, - "grantAddAccessCollectionWarning": { - "message": "Дайте правомощия за управление, за да позволите пълното управление на колекции, включително изтриването им." + "grantManageCollectionWarning": { + "message": "Дайте правомощия за управление на колекциите, за да позволите пълното управление на колекции, включително изтриването им." }, "grantCollectionAccess": { "message": "Дайте права на групи и членове до тази колекция." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "на месец за член" }, + "monthPerMemberBilledAnnually": { + "message": "месец, за всеки потребител, таксувано на годишна база" + }, "seats": { "message": "Места" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Алгоритъм на ключа" }, + "sshPrivateKey": { + "message": "Частен ключ" + }, + "sshPublicKey": { + "message": "Публичен ключ" + }, + "sshFingerprint": { + "message": "Отпечатък" + }, "sshKeyFingerprint": { "message": "Отпечатък" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Код от описанието" }, + "cannotRemoveViewOnlyCollections": { + "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Важно съобщение" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Името на организацията не може да бъде по-дълго от 50 знака." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Вашият абонамент ще бъде подновен скоро. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $RENEWAL_DATE$.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Фактура за абонамента Ви беше издадена на $ISSUED_DATE$. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $DUE_DATE$.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Фактурата за абонамента Ви не е била платена. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $GRACE_PERIOD_END$.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Администраторите вече имат възможността да изтриват акаунтите на членовете, които принадлежат към присвоен домейн." + }, + "deleteManagedUserWarningDesc": { + "message": "Това действие ще изтрие акаунта на члена, включително всички елементи в неговия трезор. Това заменя предишното действие за Премахване." + }, + "deleteManagedUserWarning": { + "message": "Изтриването е ново действие!" + }, + "seatsRemaining": { + "message": "Остават Ви $REMAINING$ от общо $TOTAL$ места в тази организация. Свържете се с доставчика си, ако искате да управлявате абонамента си.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Съществуваща организация" + }, + "selectOrganizationProviderPortal": { + "message": "Изберете организацията, която искате да добавите към своя Портал за доставчици." + }, + "noOrganizations": { + "message": "Няма организации за показване" + }, + "yourProviderSubscriptionCredit": { + "message": "Вашият абонамент за доставчик ще получи кредит за оставащото време в абонамента на организацията, ако има такова." + }, + "doYouWantToAddThisOrg": { + "message": "Искате ли да добавите тази организация към $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Добавена е съществуваща организация" + }, + "assignedExceedsAvailable": { + "message": "Назначените места превишават наличния брой." } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index b215e56e19ad..c2b6fdf46711 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "এটি কোন ধরণের বস্তু?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "ভিত্তি ডোমেইন", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "উদাহরণ", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "সংস্করণ $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "পাসওয়ার্ড উৎপাদকের কনফিগারেশনের জন্য ন্যূনতম প্রয়োজনীয়তা সেট করুন।" }, - "passwordGeneratorPolicyInEffect": { - "message": "এক বা একাধিক সংস্থার নীতিগুলি আপনার উৎপাদকের সেটিংসকে প্রভাবিত করছে।" - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 5f2605e71bc4..a4750d36eac7 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Koja je ovo vrsta stavke?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Uredite 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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Osnovni domen", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "npr.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Verzija $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 30649f3296d0..fdb1b48cd9b7 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -1,24 +1,24 @@ { "allApplications": { - "message": "All applications" + "message": "Totes les aplicacions" }, "criticalApplications": { - "message": "Critical applications" + "message": "Aplicacions crítiques" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "Intel·ligència d'accés" }, "riskInsights": { - "message": "Risk Insights" + "message": "Coneixements de risc" }, "passwordRisk": { - "message": "Password Risk" + "message": "Risc de contrasenya" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Reviseu les contrasenyes de risc (febles, exposades o reutilitzades) a totes les aplicacions. Seleccioneu les aplicacions més crítiques per prioritzar les accions de seguretat perquè els usuaris aborden les contrasenyes de risc." }, "dataLastUpdated": { - "message": "Data last updated: $DATE$", + "message": "Última actualització de les dades: $DATE$", "placeholders": { "date": { "content": "$1", @@ -27,19 +27,19 @@ } }, "notifiedMembers": { - "message": "Notified members" + "message": "Membres notificats" }, "revokeMembers": { - "message": "Revoke members" + "message": "Revoca membres" }, "restoreMembers": { - "message": "Restore members" + "message": "Restaura membres" }, "cannotRestoreAccessError": { - "message": "Cannot restore organization access" + "message": "No es pot restaurar l'accés a l'organització" }, "allApplicationsWithCount": { - "message": "All applications ($COUNT$)", + "message": "Totes les aplicacions ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -48,10 +48,10 @@ } }, "createNewLoginItem": { - "message": "Create new login item" + "message": "Crea un nou element d'inici de sessió" }, "criticalApplicationsWithCount": { - "message": "Critical applications ($COUNT$)", + "message": "Aplicacions crítiques ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -60,7 +60,7 @@ } }, "notifiedMembersWithCount": { - "message": "Notified members ($COUNT$)", + "message": "Membres notificats ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -96,25 +96,58 @@ "message": "Apps marked as critical" }, "application": { - "message": "Application" + "message": "Aplicació" }, "atRiskPasswords": { "message": "At-risk passwords" }, "requestPasswordChange": { - "message": "Request password change" + "message": "Sol·licita canvi de contrasenya" }, "totalPasswords": { - "message": "Total passwords" + "message": "Contrasenyes totals" }, "searchApps": { - "message": "Search applications" + "message": "Cerca d'aplicacions" }, "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { - "message": "Total members" + "message": "Nombre total de membres" }, "atRiskApplications": { "message": "At-risk applications" @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Quin tipus d'element és aquest?" }, @@ -160,7 +199,7 @@ "message": "Notes" }, "note": { - "message": "Note" + "message": "Nota" }, "customFields": { "message": "Camps personalitzats" @@ -172,16 +211,16 @@ "message": "Credencials d'inici de sessió" }, "personalDetails": { - "message": "Personal details" + "message": "Detalls personals" }, "identification": { - "message": "Identification" + "message": "Identificació" }, "contactInfo": { - "message": "Contact info" + "message": "Informació de contacte" }, "cardDetails": { - "message": "Card details" + "message": "Dades de la targeta" }, "cardBrandDetails": { "message": "$BRAND$ details", @@ -193,7 +232,7 @@ } }, "itemHistory": { - "message": "Item history" + "message": "Historial d'elements" }, "authenticatorKey": { "message": "Clau autenticadora" @@ -345,7 +384,7 @@ "message": "Dr." }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Targeta de crèdit caducada" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" @@ -393,17 +432,17 @@ "message": "Booleà" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Casella de selecció" }, "cfTypeLinked": { "message": "Enllaçat", "description": "This describes a field that is 'linked' (related) to another field." }, "fieldType": { - "message": "Field type" + "message": "Tipus de camp" }, "fieldLabel": { - "message": "Field label" + "message": "Etiqueta del camp" }, "remove": { "message": "Suprimeix" @@ -425,6 +464,18 @@ "editFolder": { "message": "Edita la carpeta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Domini base", "description": "Domain name. Example: website.com" @@ -469,7 +520,7 @@ "message": "Genera contrasenya" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Genera frase de pas" }, "checkPassword": { "message": "Comprova si la contrasenya ha estat exposada." @@ -572,7 +623,7 @@ "message": "Nota segura" }, "typeSshKey": { - "message": "SSH key" + "message": "Clau SSH" }, "typeLoginPlural": { "message": "Inicis de sessió" @@ -605,7 +656,7 @@ "message": "Nom complet" }, "address": { - "message": "Address" + "message": "Adreça" }, "address1": { "message": "Adreça 1" @@ -650,7 +701,7 @@ "message": "Visualitza l'element" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Nou $TYPE$", "placeholders": { "type": { "content": "$1", @@ -659,7 +710,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Edita $TYPE$", "placeholders": { "type": { "content": "$1", @@ -668,7 +719,7 @@ } }, "viewItemType": { - "message": "View $ITEMTYPE$", + "message": "Mostra $ITEMTYPE$", "placeholders": { "itemtype": { "content": "$1", @@ -689,15 +740,6 @@ "itemName": { "message": "Nom d'element" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -722,7 +764,7 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "Còpia correcta" }, "copyValue": { "message": "Copia el valor", @@ -733,11 +775,11 @@ "description": "Copy password to clipboard" }, "copyPassphrase": { - "message": "Copy passphrase", + "message": "Copia frase de pas", "description": "Copy passphrase to clipboard" }, "passwordCopied": { - "message": "Password copied" + "message": "S'ha copiat la contrasenya" }, "copyUsername": { "message": "Copia el nom d'usuari", @@ -756,7 +798,7 @@ "description": "Copy URI to clipboard" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Copia $FIELD$", "placeholders": { "field": { "content": "$1", @@ -765,34 +807,34 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copia el lloc web" }, "copyNotes": { - "message": "Copy notes" + "message": "Copia notes" }, "copyAddress": { - "message": "Copy address" + "message": "Copia l'adreça" }, "copyPhone": { - "message": "Copy phone" + "message": "Copia telèfon" }, "copyEmail": { - "message": "Copy email" + "message": "Copia el correu electrònic" }, "copyCompany": { - "message": "Copy company" + "message": "Copia empresa" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Copia número de la Seguretat Social" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Copia el número de passaport" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Copia el número de llicència" }, "copyName": { - "message": "Copy name" + "message": "Copia el nom" }, "me": { "message": "Jo" @@ -883,7 +925,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "S'han desplaçat elements a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -892,7 +934,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "S'ha desplaçat un element a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -946,25 +988,25 @@ "message": "Nivell d'accés" }, "accessing": { - "message": "Accessing" + "message": "Accedint a" }, "loggedOut": { "message": "Sessió tancada" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Heu tancat la sessió del compte." }, "loginExpired": { "message": "La vostra sessió ha caducat." }, "restartRegistration": { - "message": "Restart registration" + "message": "Reinicia el registre" }, "expiredLink": { "message": "Enllaç caducat" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "Reinicieu el registre o proveu d'iniciar sessió." }, "youMayAlreadyHaveAnAccount": { "message": "És possible que ja tingueu un compte" @@ -994,7 +1036,7 @@ "message": "L'inici de sessió amb el dispositiu ha d'estar activat a la configuració de l'aplicació Bitwarden. Necessiteu una altra opció?" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Necessiteu una altra opció?" }, "loginWithMasterPassword": { "message": "Inici de sessió amb contrasenya mestra" @@ -1009,13 +1051,13 @@ "message": "Utilitzeu un mètode d'inici de sessió diferent" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Inicieu sessió amb la clau de pas" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Usa inici de sessió únic" }, "welcomeBack": { - "message": "Welcome back" + "message": "Benvingut/da de nou" }, "invalidPasskeyPleaseTryAgain": { "message": "Clau d'accés no vàlida. Torneu-ho a provar." @@ -1099,7 +1141,7 @@ "message": "Crea un compte" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nou a Bitwarden?" }, "setAStrongPassword": { "message": "Estableix una contrasenya segura" @@ -1117,26 +1159,35 @@ "message": "Inicia sessió" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Inicia sessió a Bitwarden" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Temps d'espera d'autenticació" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "La sessió d'autenticació s'ha esgotat. Reinicieu el procés d'inici de sessió." }, "verifyIdentity": { "message": "Verificació de la vostra identitat" }, + "weDontRecognizeThisDevice": { + "message": "No reconeixem aquest dispositiu. Introduïu el codi que us hem enviat al correu electrònic per verificar la identitat." + }, + "continueLoggingIn": { + "message": "Continua l'inici de sessió" + }, "whatIsADevice": { - "message": "What is a device?" + "message": "Què és un dispositiu?" }, "aDeviceIs": { - "message": "A device is a unique installation of the Bitwarden app where you have logged in. Reinstalling, clearing app data, or clearing your cookies could result in a device appearing multiple times." + "message": "Un dispositiu és una instal·lació única de l'aplicació Bitwarden on heu iniciat la sessió. Si torneu a instal·lar, suprimir les dades de l'aplicació o suprimir les galetes, pot ser que un dispositiu aparega diverses vegades." }, "logInInitiated": { "message": "S'ha iniciat la sessió" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Envia" }, @@ -1168,7 +1219,7 @@ "message": "Pista de la contrasenya mestra (opcional)" }, "newMasterPassHint": { - "message": "New master password hint (optional)" + "message": "Pista de la contrasenya mestra (opcional)" }, "masterPassHintLabel": { "message": "Pista de la contrasenya mestra" @@ -1190,16 +1241,16 @@ "message": "Configuració" }, "accountEmail": { - "message": "Account email" + "message": "Correu electrònic del compte" }, "requestHint": { - "message": "Request hint" + "message": "Sol·licita pista" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Sol·licita pista de la contrasenya" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Introduïu l'adreça de correu electrònic del compte i se us enviarà la pista de contrasenya" }, "passwordHint": { "message": "Pista de la contrasenya" @@ -1239,10 +1290,10 @@ "message": "El vostre compte s'ha creat correctament. Ara ja podeu entrar." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "S'ha creat el vostre compte nou!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Heu iniciat sessió!" }, "trialAccountCreated": { "message": "Compte creat amb èxit." @@ -1263,7 +1314,7 @@ "message": "La caixa forta està bloquejada" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "El compte està bloquejat" }, "uuid": { "message": "UUID" @@ -1326,11 +1377,38 @@ "notificationSentDevice": { "message": "S'ha enviat una notificació al vostre dispositiu." }, - "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirmeu l'accés" + }, + "denyAccess": { + "message": "Denega l'accés" + }, + "notificationSentDeviceAnchor": { + "message": "aplicació web" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, + "aNotificationWasSentToYourDevice": { + "message": "S'ha enviat una notificació al vostre dispositiu" }, "versionNumber": { "message": "Versió $VERSION_NUMBER$", @@ -1406,7 +1484,7 @@ "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." + "message": "Utilitzeu un dispositiu YubiKey 4, 5 o NEO." }, "duoDescV2": { "message": "Enter a code generated by Duo Security.", @@ -1423,10 +1501,10 @@ "message": "Clau de seguretat FIDO U2F" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "Clau d'accés" }, "webAuthnDesc": { - "message": "Utilitzeu qualsevol clau de seguretat habilitada per WebAuthn per accedir al vostre compte." + "message": "Utilitzeu la biometria del vostre dispositiu o una clau de seguretat compatible amb FIDO2." }, "webAuthnMigrated": { "message": "(Migrat de FIDO)" @@ -1435,7 +1513,7 @@ "message": "Correu electrònic" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Introduïu el codi que us hem enviat al correu electrònic." }, "continue": { "message": "Continua" @@ -1477,7 +1555,7 @@ "message": "Esteu segur que voleu continuar?" }, "moveSelectedItemsDesc": { - "message": "Trieu una carpeta a la que vulgueu afegir els $COUNT$ elements seleccionats.", + "message": "Trieu una carpeta a la qual vulgueu afegir els $COUNT$ elements seleccionats.", "placeholders": { "count": { "content": "$1", @@ -1616,12 +1694,9 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Eviteu caràcters ambigus", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenera contrasenya" - }, "length": { "message": "Longitud" }, @@ -1657,29 +1732,29 @@ "message": "Inclou número" }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "Els requisits de la política empresarial s'han aplicat a les opcions del generador.", "description": "Indicates that a policy limits the credential generator screen." }, "passwordHistory": { "message": "Historial de les contrasenyes" }, "generatorHistory": { - "message": "Generator history" + "message": "Historial del generador" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Neteja l'historial del generador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Si continueu, totes les entrades se suprimiran permanentment de l'historial del generador. Esteu segur que voleu continuar?" }, "noPasswordsInList": { "message": "No hi ha cap contrasenya a llistar." }, "clearHistory": { - "message": "Clear history" + "message": "Neteja l'historial" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Res a mostrar" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -1722,10 +1797,10 @@ "message": "Torneu a iniciar sessió." }, "currentSession": { - "message": "Current session" + "message": "Sessió actual" }, "requestPending": { - "message": "Request pending" + "message": "Sol·licitud pendent" }, "logBackInOthersToo": { "message": "Torneu a iniciar la sessió. Si esteu utilitzant altres aplicacions Bitwarden, tanqueu-les i torneu-les a obrir també." @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Zona perillosa" }, - "dangerZoneDesc": { - "message": "Aneu amb compte, aquestes accions no són reversibles!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Desautoritza sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "El procediment també tancarà la sessió actual, i l'heu de tornar a iniciar. També demanarà iniciar la sessió en dues passes, si està habilitada. Les sessions actives d'altres dispositius poden mantenir-se actives fins a una hora." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Totes les sessions estan desautoritzades" }, @@ -1878,7 +1968,7 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsLoginLink": { - "message": "new login", + "message": "inici de sessió nou", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new login instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { @@ -2110,8 +2200,20 @@ "manage": { "message": "Administra" }, - "canManage": { - "message": "Pot gestionar" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Inhabilita" @@ -2132,7 +2234,7 @@ "message": "," }, "twoStepAuthenticatorInstructionInfix2": { - "message": "or" + "message": "o" }, "twoStepAuthenticatorInstructionSuffix": { "message": "." @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Hi ha hagut un problema en llegir la clau de seguretat. Torneu-ho a provar." }, - "twoFactorWebAuthnWarning": { - "message": "A causa de les limitacions de la plataforma, WebAuthn no es pot utilitzar en totes les aplicacions Bitwarden. Heu d’habilitar un altre proveïdor d’inici de sessió en dos passos perquè pugueu accedir al vostre compte quan no es puga utilitzar WebAuthn. Plataformes compatibles:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Caixa forta web i extensions de navegador en un escriptori/portàtil amb un navegador compatible amb WebAuthn (Chrome, Opera, Vivaldi, o Firefox amb FIDO U2F activat)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "El codi de recuperació d'inici de sessió en dues passes de Bitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Aquest usuari fa servir l'inici de sessió en dues passes per protegir el seu compte." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "SSO sense enllaçar per a l'usuari $ID$.", "placeholders": { @@ -3789,11 +3894,72 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Ara mateix" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, "checkYourEmail": { - "message": "Check your email" + "message": "Comprova el correu" }, "followTheLinkInTheEmailSentTo": { "message": "Follow the link in the email sent to" @@ -4355,7 +4521,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:" @@ -4376,7 +4542,7 @@ "message": "Temps d'espera de la caixa forta" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Temps d'espera" }, "vaultTimeoutDesc": { "message": "Trieu quan es tancarà la vostra caixa forta i feu l'acció seleccionada." @@ -4445,7 +4611,7 @@ "message": "Seleccionat" }, "recommended": { - "message": "Recommended" + "message": "Recomanat" }, "ownership": { "message": "Propietat" @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Estableix els requisits mínims per a la configuració del generador de contrasenyes." }, - "passwordGeneratorPolicyInEffect": { - "message": "Una o més polítiques d’organització afecten la configuració del generador." - }, "masterPasswordPolicyInEffect": { "message": "Una o més polítiques d’organització requereixen que la vostra contrasenya principal complisca els requisits següents:" }, @@ -4932,7 +5095,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copyLink": { - "message": "Copy link" + "message": "Copia l'enllaç" }, "copySendLink": { "message": "Copia l'enllaç Send", @@ -5438,7 +5601,7 @@ } }, "viewSend": { - "message": "View Send", + "message": "Veure Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -6554,15 +6717,6 @@ "message": "Generador", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Què voleu generar?" - }, - "passwordType": { - "message": "Tipus de contrasenya" - }, - "regenerateUsername": { - "message": "Regenera el nom d'usuari" - }, "generateUsername": { "message": "Genera un nom d'usuari" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Tipus de nom d'usuari" - }, "plusAddressedEmail": { "message": "Adreça amb sufix", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Utilitzeu la safata d'entrada global configurada del vostre domini." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Aleatori", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Nom de l'amfitrió", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token d'accés a l'API" - }, "deviceVerification": { "message": "Verificació del dispositiu" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Cap col·lecció" }, - "canView": { - "message": "Pot veure" - }, - "canViewExceptPass": { - "message": "Pot veure, excepte les contrasenyes" - }, - "canEdit": { - "message": "Pot editar" - }, - "canEditExceptPass": { - "message": "Pot editar, excepte les contrasenyes" - }, "noCollectionsAdded": { "message": "No s'han afegit col·leccions" }, @@ -7840,7 +7979,7 @@ "message": "Càrrega de fitxers" }, "upload": { - "message": "Upload" + "message": "Puja" }, "acceptedFormats": { "message": "Formats acceptats:" @@ -7852,10 +7991,10 @@ "message": "o" }, "unlockWithBiometrics": { - "message": "Unlock with biometrics" + "message": "Desbloqueja amb dades biomètriques" }, "unlockWithPin": { - "message": "Unlock with PIN" + "message": "Desbloqueja amb codi PIN" }, "unlockWithMasterPassword": { "message": "Unlock with master password" @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Dispositius de confiança" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Una vegada autenticats, els membres desxifraran les dades de la caixa forta mitjançant una clau emmagatzemada al seu dispositiu. La", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "política d'organització", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "única,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "la política de", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "requerida y la política", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "d'administració de recuperació del compte", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "amb inscripció automàtica s'activaran quan s'utilitze aquesta opció.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Els permisos de la vostra organització s'han actualitzat, cal que establiu una contrasenya mestra.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Els propietaris i els administradors poden gestionar totes les col·leccions i articles" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alies de domini" - }, "alreadyHaveAccount": { "message": "Ja tens un compte?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "No teniu accés per gestionar aquesta col·lecció." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Concedeix als grups o membres l'accés a aquesta col·lecció." @@ -8780,7 +8919,7 @@ "message": "Els ajustos dels seients es reflectiran en el pròxim cicle de facturació." }, "unassignedSeatsDescription": { - "message": "Seients de subscripció no assignats" + "message": "Places de subscripció no assignades" }, "purchaseSeatDescription": { "message": "Seients addicionals adquirits" @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "mes per membre" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seients" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 7c8978fac802..4fa1a50512cd 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -113,15 +113,54 @@ "atRiskMembers": { "message": "Ohrožení členové" }, + "atRiskMembersWithCount": { + "message": "Rizikoví členové ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Rizikové aplikace ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Tito členové se přihlašují do aplikací se slabými, odhalenými nebo znovu použitými hesly." + }, + "atRiskApplicationsDescription": { + "message": "Tyto aplikace mají slabá, odhalená nebo opakovaně používaná hesla." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Tito členové se přihlašují do $APPNAME$ se slabými, odhalenými nebo znovu použitými hesly.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Celkem členů" }, "atRiskApplications": { - "message": "Ohrožené aplikace" + "message": "Rizikové aplikace" }, "totalApplications": { "message": "Celkem aplikací" }, + "unmarkAsCriticalApp": { + "message": "Zrušit označení jako kritické aplikace" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritická aplikace úspěšně odoznačena" + }, "whatTypeOfItem": { "message": "O jaký typ položky se jedná?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Upravit složku" }, + "newFolder": { + "message": "Nová složka" + }, + "folderName": { + "message": "Název složky" + }, + "folderHintText": { + "message": "Vnořte složku přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra" + }, + "deleteFolderPermanently": { + "message": "Opravdu chcete trvale smazat tuto složku?" + }, "baseDomain": { "message": "Základní doména", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Název položky" }, - "cannotRemoveViewOnlyCollections": { - "message": "Nemůžete odebrat kolekce s oprávněními jen pro zobrazení: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "např.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Ověřte svou totožnost" }, + "weDontRecognizeThisDevice": { + "message": "Toto zařízení nepoznáváme. Zadejte kód zaslaný na Váš e-mail pro ověření Vaší totožnosti." + }, + "continueLoggingIn": { + "message": "Pokračovat v přihlášení" + }, "whatIsADevice": { "message": "Co je to zařízení?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Bylo zahájeno přihlášení" }, + "logInRequestSent": { + "message": "Požadavek odeslán" + }, "submit": { "message": "Odeslat" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Na Vaše zařízení bylo odesláno oznámení." }, + "notificationSentDevicePart1": { + "message": "Odemknout Bitwarden na Vašem zařízení nebo na " + }, + "areYouTryingToAccessYourAccount": { + "message": "Pokoušíte se získat přístup k Vašemu účtu?" + }, + "accessAttemptBy": { + "message": "Pokus o přístup z $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Potvrdit přístup" + }, + "denyAccess": { + "message": "Zamítnout přístup" + }, + "notificationSentDeviceAnchor": { + "message": "webová aplikace" + }, + "notificationSentDevicePart2": { + "message": "Před schválením se ujistěte, že fráze otisku prstu odpovídá frázi níže." + }, + "notificationSentDeviceComplete": { + "message": "Odemkněte Bitwarden na Vašem zařízení. Před schválením se ujistěte, že fráze otisku prstu odpovídá frázi níže." + }, "aNotificationWasSentToYourDevice": { "message": "Na Vaše zařízení bylo odesláno oznámení" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Ujistěte se, že je Váš trezor odemčen a fráze otisku prstu se shodují s druhým zařízením" - }, "versionNumber": { "message": "Verze $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Nepoužívat zaměnitelné znaky", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Vygenerovat jiné heslo" - }, "length": { "message": "Délka" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Nebezpečná zóna" }, - "dangerZoneDesc": { - "message": "Opatrně. Tyto akce se nedají vrátit!" - }, - "dangerZoneDescSingular": { - "message": "Opatrně, tato akce je nevratná!" - }, "deauthorizeSessions": { "message": "Zrušit autorizaci relací" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Pokud chcete pokračovat, budete také odhlášeni z aktuální relace a bude nutné se znovu přihlásit. Pokud používáte dvoufázové přihlášení, bude také vyžadováno. Aktivní relace na jiných zařízeních mohou nadále zůstat aktivní po dobu až jedné hodiny." }, + "newDeviceLoginProtection": { + "message": "Nové přihlášení do zařízení" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Vypnout ochranu pro přihlášení z nového zařízení" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Zapnout ochranu pro přihlášení z nového zařízení" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Pokračujte níže a vypněte ověřovací e-maily od Bitwardenu, když se přihlásíte z nového zařízení." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Níže postupujte tak, aby Vám Bitwarden zasílal ověřovací e-maily při přihlášení z nového zařízení." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Pokud je ochrana přihlášení z nového zařízení vypnutá, může kdokoli s Vaším hlavním heslem přistupovat k Vašemu účtu z libovolného zařízení. Chcete-li svůj účet chránit bez ověřovacích e-mailů, nastavte dvoufázové přihlašování." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Změny ochrany přihlášení z nového zařízení byly uloženy" + }, "sessionsDeauthorized": { "message": "Všechny relace byly zrušeny" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Spravovat" }, - "canManage": { - "message": "Může spravovat" + "manageCollection": { + "message": "Spravovat kolekci" + }, + "viewItems": { + "message": "Zobrazit položky" + }, + "viewItemsHidePass": { + "message": "Zobrazit položky, skrytá hesla" + }, + "editItems": { + "message": "Upravit položky" + }, + "editItemsHidePass": { + "message": "Upravit položky, skrytá hesla" }, "disable": { "message": "Vypnout" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Při čtení bezpečnostního klíče došlo k potížím. Zkuste to znovu." }, - "twoFactorWebAuthnWarning": { - "message": "Z důvodu omezení platformy nelze WebAuthn použít ve všech aplikacích Bitwarden. Měli byste využít i jiného poskytovatele dvoufaktorového přihlášení, abyste měli přístup ke svému účtu, když nelze použít WebAuthn. Podporované platformy:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Webový trezor a rozšíření pro prohlížeče na počítači/notebooku s podporou WebAuthn (Chrome, Opera, Vivaldi nebo Firefox se zapnutým FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Z důvodu omezení platformy nelze WebAuthn použít ve všech aplikacích Bitwarden. Měli byste využít i jiného poskytovatele dvoufaktorového přihlášení, abyste měli přístup ke svému účtu, když nelze použít WebAuthn." }, "twoFactorRecoveryYourCode": { "message": "Váš kód pro obnovení dvoufázového přihlášení" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Zbývá Vám 1 pozvánka." }, + "inviteZeroEmailDesc": { + "message": "Zbývá Vám 0 pozvánek." + }, "userUsingTwoStep": { "message": "Tento uživatel používá pro ochranu svého účtu dvoufázové přihlášení." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Nepropojené SSO." + }, "unlinkedSsoUser": { "message": "Bylo zrušeno propojení SSO pro uživatele $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Důvěryhodný" }, + "needsApproval": { + "message": "Vyžaduje schválení" + }, + "areYouTryingtoLogin": { + "message": "Pokoušíte se přihlásit?" + }, + "logInAttemptBy": { + "message": "Pokus o přihlášení z $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Typ zařízení" + }, + "ipAddress": { + "message": "IP adresa" + }, + "confirmLogIn": { + "message": "Potvrdit přihlášení" + }, + "denyLogIn": { + "message": "Zamítnout přihlášení" + }, + "thisRequestIsNoLongerValid": { + "message": "Tento požadavek již není platný." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Přihlášení bylo potvrzeno z $EMAIL$ pro $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Pokus o přihlášení byl zamítnut z jiného zařízení. Pokud jste to opravdu Vy, zkuste se znovu přihlásit do zařízení." + }, + "loginRequestHasAlreadyExpired": { + "message": "Požadavek na přihlášení již vypršel." + }, + "justNow": { + "message": "Právě teď" + }, + "requestedXMinutesAgo": { + "message": "Požadováno před $MINUTES$ minutami", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Vytváření účtu na" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Nastaví požadavky pro generátor hesel." }, - "passwordGeneratorPolicyInEffect": { - "message": "Jedna nebo více zásad organizace ovlivňují nastavení generátoru." - }, "masterPasswordPolicyInEffect": { "message": "Jedna nebo více zásad organizace vyžaduje, aby hlavní heslo splňovalo následující požadavky:" }, @@ -6554,15 +6717,6 @@ "message": "Generátor", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Co chcete vygenerovat?" - }, - "passwordType": { - "message": "Typ hesla" - }, - "regenerateUsername": { - "message": "Znovu vygenerovat uživatelské jméno" - }, "generateUsername": { "message": "Vygenerovat uživatelské jméno" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Typ uživatelského jména" - }, "plusAddressedEmail": { "message": "E-mailová adresa s plusem", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Použijte nakonfigurovanou univerzální schránku své domény." }, + "useThisEmail": { + "message": "Použít tento e-mail" + }, "random": { "message": "Náhodně", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Název hostitele", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Přístupový token API" - }, "deviceVerification": { "message": "Ověřování zařízení" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Žádná kolekce" }, - "canView": { - "message": "Může zobrazit" - }, - "canViewExceptPass": { - "message": "Může zobrazit kromě hesel" - }, - "canEdit": { - "message": "Může upravovat" - }, - "canEditExceptPass": { - "message": "Může upravovat kromě hesel" - }, "noCollectionsAdded": { "message": "Nebyly přidány žádné kolekce" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Důvěryhodná zařízení" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Po ověření budou členové dešifrovat data v trezoru pomocí klíče uloženého na jejich zařízení. ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "Při použití této možnosti ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "se zapnou zásady jednotné organizace, ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "zásady vyžadované SSO ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "a zásady správy obnovení účtu ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "s ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "s automatickým zápisem.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Oprávnění Vaší organizace byla aktualizována. To vyžaduje nastavení hlavního hesla.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Omezí mazání kolekce na vlastníky a správce" }, + "limitItemDeletionDesc": { + "message": "Omezit smazání položky na členy s oprávněním ke správě" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Vlastníci a správci mohou spravovat všechny kolekce a předměty" }, @@ -8544,9 +8686,6 @@ "message": "Adresa URL serveru vlastního hostování", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Doména aliasu" - }, "alreadyHaveAccount": { "message": "Už máte účet?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Nemáte přístup ke správě této kolekce." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Chybějící oprávnění \"Může spravovat\"" + "grantManageCollectionWarningTitle": { + "message": "Chybějící oprávnění pro správu kolekcí" }, - "grantAddAccessCollectionWarning": { - "message": "Udělte oprávnění \"Může spravovat\" pro úplnou správu kolekce, včetně jejího smazání." + "grantManageCollectionWarning": { + "message": "Udělte oprávnění \"Spravovat kolekci\" pro úplnou správu kolekce, včetně jejího smazání." }, "grantCollectionAccess": { "message": "Udělí skupinám nebo členům přístup k této kolekci." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "měsíčně za člena" }, + "monthPerMemberBilledAnnually": { + "message": "měsíčně za člena a účtováno ročně" + }, "seats": { "message": "Počet" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Algoritmus klíče" }, + "sshPrivateKey": { + "message": "Soukromý klíč" + }, + "sshPublicKey": { + "message": "Veřejný klíč" + }, + "sshFingerprint": { + "message": "Otisk prstu" + }, "sshKeyFingerprint": { "message": "Otisk prstu" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Kód z popisu" }, + "cannotRemoveViewOnlyCollections": { + "message": "Nemůžete odebrat kolekce s oprávněními jen pro zobrazení: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Důležité upozornění" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Název organizace nesmí přesáhnout 50 znaků." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Vaše předplatné se brzy obnoví. Chcete-li zajistit nepřerušenou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $RENEWAL_DATE$.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Faktura pro Vaše předplatné byla vystavena dne $ISSUED_DATE$. Chcete-li zajistit nepřerušovanou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $DUE_DATE$.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Faktura za Vaše předplatné nebyla zaplacena. Chcete-li zajistit nepřerušovanou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $GRACE_PERIOD_END$.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Správci mají nyní možnost smazat účty členů, které patří k nárokované doméně." + }, + "deleteManagedUserWarningDesc": { + "message": "Tato akce smaže účet uživatele včetně všech jeho položek v trezoru. Tím se nahradí předchozí akce odebrání." + }, + "deleteManagedUserWarning": { + "message": "Smazat je nová akce!" + }, + "seatsRemaining": { + "message": "Máte zbývajících $REMAINING$ uživatelů z $TOTAL$ uživatelů přidělených této organizaci. Kontaktujte svého poskytovatele pro správu předplatného.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existující organizace" + }, + "selectOrganizationProviderPortal": { + "message": "Vyberte organizaci pro přidání do portálu poskytovatele." + }, + "noOrganizations": { + "message": "Žádné organizace k zobrazení" + }, + "yourProviderSubscriptionCredit": { + "message": "Předplatné poskytovatele obdrží kredit za zbývající čas v předplatném organizace." + }, + "doYouWantToAddThisOrg": { + "message": "Chcete přidat tuto organizaci do $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Přidána existující organizace" + }, + "assignedExceedsAvailable": { + "message": "Přiřazení uživatelé překračují dostupné uživatele." } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index ed8a03eb8b04..01c4f390ddc3 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 9731c344e6ed..bd357b4cba4c 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Udsatte medlemmer" }, + "atRiskMembersWithCount": { + "message": "Udsatte medlemmer ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Udsatte applikationer ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Disse medlemmer logger ind på programmer med svage, udsatte eller genbrugte adgangskoder." + }, + "atRiskApplicationsDescription": { + "message": "Disse applikationer har svage, udsatte eller genbrugte adgangskoder." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Disse medlemmer logger ind på $APPNAME$ med svage, udsatte eller genbrugte adgangskoder.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Medlemmer i alt" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Applikationer i alt" }, + "unmarkAsCriticalApp": { + "message": "Afmarkér som kritisk app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritisk app afmarkeret" + }, "whatTypeOfItem": { "message": "Hvilken emnetype er denne?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Redigér mappe" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Basisdomæne", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Emnenavn" }, - "cannotRemoveViewOnlyCollections": { - "message": "Samlinger med kun tilladelsen Vis kan ikke fjernes: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "eks.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Bekræft din identitet" }, + "weDontRecognizeThisDevice": { + "message": "Denne enhed er ikke genkendt. Angiv koden i den tilsendte e-mail for at bekræfte identiteten." + }, + "continueLoggingIn": { + "message": "Fortsæt med at logge ind" + }, "whatIsADevice": { "message": "Hvad er en enhed?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Indlogning påbegyndt" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Indsend" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "En notifikation er sendt til din enhed." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "En notifikation er sendt til enheden" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Sørg for, at boksen er oplåst, samt at fingeraftrykssætningen matcher på den anden enhed" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Undgå tvetydige tegn", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerér adgangskode" - }, "length": { "message": "Længde" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Farezone" }, - "dangerZoneDesc": { - "message": "Pas på, disse handlinger er irreversible!" - }, - "dangerZoneDescSingular": { - "message": "Forsigtig, denne handling er irreversibel!" - }, "deauthorizeSessions": { "message": "Fjern sessionsgodkendelser" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Fortsættes, logges man også ud af denne session og vil skulle logge ind igen. Der anmodes også om totrins-login igen, såfremt funktionen er opsat. Aktive sessioner på andre enheder kan forblive aktive i op til én time." }, + "newDeviceLoginProtection": { + "message": "Nyt enheds-login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Slå nyt enheds-login beskyttelse fra" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Slå nyt enheds-login beskyttelse til" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Fortsæt nedenfor for at deaktivere Bitwarden-bekræftelsesmails ved indlogning fra en ny enhed." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Fortsæt nedenfor for at få tilsendt Bitwarden-bekræftelsesmails ved indlogning fra en ny enhed." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Med den nye enheds-login beskyttelse slået fra kan alle med hovedadgangskoden tilgå brugerkontoen fra enhver enhed. For at beskytte brugerkontoen uden bekræftelsesmails, opsæt totrins-login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Ændringer i ny enheds-login beskyttelse er gemt" + }, "sessionsDeauthorized": { "message": "Godkendelser for alle sessioner fjernet" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Håndtér" }, - "canManage": { - "message": "Kan håndtere" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Deaktivér" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Problem med at læse sikkerhedsnøglen. Forsøg igen." }, - "twoFactorWebAuthnWarning": { - "message": "Grundet platformsbegrænsninger kan WebAuthn ikke bruges i alle Bitwarden-applikationer. Man bør opsætte en anden totrins-loginudbyder, så man kan tilgå sin konto, når WebAuthn ikke kan benyttes. Understøttede platforme:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web-boks og browserudvidelser på en stationær/bærbar computer med en WebAuthn-aktiveret browser (Chrome, Opera, Vivaldi eller Firefox med FIDO U2F aktiveret)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Gendannelseskoden til Bitwarden totrins-login" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Der er 1 invitation tilbage." }, + "inviteZeroEmailDesc": { + "message": "Der er 0 invitationer tilbage." + }, "userUsingTwoStep": { "message": "Denne bruger benytter totrins-login for at beskytte kontoen." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Fjern SSO-tilknytning." + }, "unlinkedSsoUser": { "message": "Af-linket SSO for bruger $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Betroet" }, + "needsApproval": { + "message": "Kræver godkendelse" + }, + "areYouTryingtoLogin": { + "message": "Forsøger at logge ind?" + }, + "logInAttemptBy": { + "message": "Loginforsøg fra $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Enhedstype" + }, + "ipAddress": { + "message": "IP-adresse" + }, + "confirmLogIn": { + "message": "Bekræft login" + }, + "denyLogIn": { + "message": "Afvis login" + }, + "thisRequestIsNoLongerValid": { + "message": "Anmodningen er ikke længere gyldig." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login bekræftet for $EMAIL$ på $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Du nægtede et loginforsøg fra en anden enhed. Hvis dette virkelig var dig, prøv at logge ind med enheden igen." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login-anmodning er allerede udløbet." + }, + "justNow": { + "message": "Netop nu" + }, + "requestedXMinutesAgo": { + "message": "Anmodet for $MINUTES$ minutter siden", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Opretter konto på" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Angiv krav til adgangskodegenerator." }, - "passwordGeneratorPolicyInEffect": { - "message": "Én eller flere organisationspolitikker påvirker dine generatorindstillinger." - }, "masterPasswordPolicyInEffect": { "message": "Én eller flere organisationspolitikker kræver din hovedadgangskode opfylder følgende krav:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Hvad ønskes genereret?" - }, - "passwordType": { - "message": "Adgangskodetype" - }, - "regenerateUsername": { - "message": "Regenerér brugernavn" - }, "generateUsername": { "message": "Generér brugernavn" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Brugernavnstype" - }, "plusAddressedEmail": { "message": "Plus adresseret e-mail", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Brug den for domænet opsatte Fang-alle indbakke." }, + "useThisEmail": { + "message": "Benyt denne e-mail" + }, "random": { "message": "Tilfældig", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Værtsnavn", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API-adgangstoken" - }, "deviceVerification": { "message": "Enhedsbekræftelse" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Ingen samling" }, - "canView": { - "message": "Kan se" - }, - "canViewExceptPass": { - "message": "Kan se, undtagen adgangskoder" - }, - "canEdit": { - "message": "Kan redigere" - }, - "canEditExceptPass": { - "message": "Kan redigere, undtagen adgangskoder" - }, "noCollectionsAdded": { "message": "Ingen samlinger tilføjet" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Betroede enheder" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Når godkendt, dekrypterer medlemmet boksdata vha. en nøgle gemt på deres enhed. ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Medlemmer behøver ikke en hovedadgangskode ved indlogning med SSO. Hovedadgangskoden erstattes af en krypteringsnøgle gemt på enheden, hvilket gør enheden betroet. Den første enhed, hvorfra et medlem opretter sin konto og logger ind, vil være betroet. Nye enheder skal godkendes af en eksisterende betroet enhed eller af en administrator. Den", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "Den enkelte organisations", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "enkelte organisations", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "politik,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO-påkrævede", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO-krævet", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "politik samt", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "politik og", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "kontogendannelseshåndteringspolitik", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "kontogendannelseshåndterings-", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "med automatisk indrullering slås til ved brug af denne indstilling.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "politik træder i kraft, når denne indstilling bruges.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Organisationstilladelserne er blevet opdateret, og der kræves nu oprettelse af en hovedadgangskode.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Begræns samlingsslettelse til ejere og admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Ejere og admins kan håndtere alle samlinger og emner" }, @@ -8544,9 +8686,6 @@ "message": "URL til selv-hostet server", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Aliasdomæne" - }, "alreadyHaveAccount": { "message": "Har allerede en konto?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Du har ikke adgang til at håndtere denne samling." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Manglende Kan håndtere tilladelser" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Tildel Kan håndtere tilladelser for at tillade fuld samlingshåndtering, herunder sletning af samling." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Tildel grupper eller medlemmer adgang til denne samling." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "måned pr. medlem" }, + "monthPerMemberBilledAnnually": { + "message": "måned pr. medlem faktureret årligt" + }, "seats": { "message": "Pladser" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Nøglealgoritme" }, + "sshPrivateKey": { + "message": "Privat nøgle" + }, + "sshPublicKey": { + "message": "Offentlig nøgle" + }, + "sshFingerprint": { + "message": "Fingeraftryk" + }, "sshKeyFingerprint": { "message": "Fingeraftryk" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Beskrivelseskode" }, + "cannotRemoveViewOnlyCollections": { + "message": "Samlinger med kun tilladelsen Vis kan ikke fjernes: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Vigtig notits" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organisationsnavn må ikke overstige 50 tegn." }, - "resellerRenewalWarning": { - "message": "Abonnementet fornyes snart. For at sikre uafbrudt tjeneste, kontakt $RESELLER$ for at bekræfte fornyelsen inden $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Abonnementet fornyes snart. For at sikre uafbrudt tjeneste, kontakt $RESELLER$ inden $RENEWAL_DATE$ for at bekræfte fornyelsen.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "En faktura for abonnementet er udstedt pr. $ISSUED_DATE$. For at sikre uafbrudt tjeneste, kontakt $RESELLER$ for at bekræfte fornyelsen inden $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "En abonnementsfaktura er udstedt pr. $ISSUED_DATE$. For at sikre uafbrudt tjeneste, kontakt $RESELLER$ inden $DUE_DATE$ for at bekræfte fornyelsen.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "Fakturaen for abonnementet er ikke blevet betalt. For at sikre uafbrudt tjeneste, kontakt $RESELLER$ for at bekræfte fornyelsen inden $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "Abonnementsfakturaen er ikke blevet betalt. For at sikre uafbrudt tjeneste, kontakt $RESELLER$ inden $GRACE_PERIOD_END$ for at bekræfte fornyelsen.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 540db2dec052..ffc65be543ca 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Risikoreiche Mitglieder" }, + "atRiskMembersWithCount": { + "message": "Risikoreiche Mitglieder ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Risikoreiche Anwendungen ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Diese Mitglieder melden sich bei Anwendungen mit schwachen, kompromittierten oder wiederverwendeten Passwörtern an." + }, + "atRiskApplicationsDescription": { + "message": "Diese Anwendungen haben schwache, kompromittierte oder wiederverwendete Passwörter." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Diese Mitglieder melden sich bei $APPNAME$ mit schwachen, kompromittierten oder wiederverwendeten Passwörtern an.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Mitglieder insgesamt" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Anwendungen insgesamt" }, + "unmarkAsCriticalApp": { + "message": "Markierung als kritische Anwendung aufheben" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Markierung als kritische Anwendung erfolgreich aufgehoben" + }, "whatTypeOfItem": { "message": "Um welche Art von Eintrag handelt es sich hierbei?" }, @@ -425,6 +464,18 @@ "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: Sozial/Foren" + }, + "deleteFolderPermanently": { + "message": "Bist du sicher, dass du diesen Ordner dauerhaft löschen willst?" + }, "baseDomain": { "message": "Basisdomäne", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Eintrags-Name" }, - "cannotRemoveViewOnlyCollections": { - "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "Bsp.", "description": "Short abbreviation for 'example'." @@ -1128,15 +1170,24 @@ "verifyIdentity": { "message": "Verifiziere deine Identität" }, + "weDontRecognizeThisDevice": { + "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." + }, + "continueLoggingIn": { + "message": "Anmeldung fortsetzen" + }, "whatIsADevice": { - "message": "Was ist ein \"Gerät\"?" + "message": "Was ist ein Gerät?" }, "aDeviceIs": { - "message": "Ein \"Gerät\" ist eine einzigartige Installation der Bitwarden-App, in der du dich angemeldet hast. Eine Neuinstallation, das Löschen von App-Daten oder das Löschen von Cookies könnte dazu führen, dass ein \"Gerät\" mehrfach erscheint." + "message": "Ein Gerät ist eine einzigartige Installation der Bitwarden-App, in der du dich angemeldet hast. Eine Neuinstallation, das Löschen von App-Daten oder das Löschen von Cookies könnte dazu führen, dass ein Gerät mehrfach erscheint." }, "logInInitiated": { "message": "Anmeldung eingeleitet" }, + "logInRequestSent": { + "message": "Anfrage gesendet" + }, "submit": { "message": "Absenden" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Eine Benachrichtigung wurde an dein Gerät gesendet." }, + "notificationSentDevicePart1": { + "message": "Entsperre Bitwarden auf deinem Gerät oder mit der " + }, + "areYouTryingToAccessYourAccount": { + "message": "Versuchst du auf dein Konto zuzugreifen?" + }, + "accessAttemptBy": { + "message": "Zugriffsversuch von $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Zugriff bestätigen" + }, + "denyAccess": { + "message": "Zugriff ablehnen" + }, + "notificationSentDeviceAnchor": { + "message": "Web-App" + }, + "notificationSentDevicePart2": { + "message": "Stelle vor der Genehmigung sicher, dass die Fingerabdruck-Phrase mit der unten stehenden übereinstimmt." + }, + "notificationSentDeviceComplete": { + "message": "Entsperre Bitwarden auf deinem Gerät. Stelle vor der Genehmigung sicher, dass die Fingerabdruck-Phrase mit der unten stehenden übereinstimmt." + }, "aNotificationWasSentToYourDevice": { "message": "Eine Benachrichtigung wurde an dein Gerät gesendet" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Stelle sicher, dass dein Konto entsperrt ist und die Fingerabdruck-Phrase mit der vom anderen Gerät übereinstimmt" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Mehrdeutige Zeichen vermeiden", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Passwort neu generieren" - }, "length": { "message": "Länge" }, @@ -1794,21 +1869,36 @@ "dangerZone": { "message": "Gefahrenzone" }, - "dangerZoneDesc": { - "message": "Vorsicht, diese Aktionen sind nicht umkehrbar!" - }, - "dangerZoneDescSingular": { - "message": "Vorsicht, diese Aktion ist nicht mehr rückgängig zu machen!" - }, "deauthorizeSessions": { "message": "Sitzungen abmelden" }, "deauthorizeSessionsDesc": { - "message": "Bist du besorgt, dass dein Konto auf einem anderen Gerät angemeldet ist? Fahre unten fort, um alle Computer oder Geräte, die du zuvor verwendet hast, zu deaktivieren. Dieser Sicherheitsschritt wird empfohlen, wenn du zuvor einen öffentlichen Computer verwendet oder dein Passwort versehentlich auf einem Gerät gespeichert hast, das dir nicht gehört. Dieser Schritt löscht auch alle zuvor gespeicherten zweistufigen Anmeldesitzungen." + "message": "Bist du besorgt, dass dein Konto auf einem anderen Gerät angemeldet ist? Fahre unten fort, um dich von allen Computern oder Geräten, die du zuvor verwendet hast, abzumelden. Dieser Sicherheitsschritt wird empfohlen, wenn du zuvor einen öffentlichen Computer verwendet oder dein Passwort versehentlich auf einem Gerät gespeichert hast, das dir nicht gehört. Dieser Schritt löscht auch alle zuvor gespeicherten zweistufigen Anmeldesitzungen." }, "deauthorizeSessionsWarning": { "message": "Wenn du fortfährst, wirst du auch von deiner aktuellen Sitzung abgemeldet, so dass du dich erneut anmelden musst. Du wirst auch aufgefordert, dich erneut in zwei Schritten anzumelden, falls dies eingerichtet ist. Aktive Sitzungen auf anderen Geräten können noch bis zu einer Stunde lang aktiv bleiben." }, + "newDeviceLoginProtection": { + "message": "Neue Geräteanmeldung" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Neuen Geräte-Anmeldeschutz deaktivieren" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Neuen Geräte-Anmeldeschutz aktivieren" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Fahre unten fort, um die Verifizierungs-E-Mails von Bitwarden zu deaktivieren, wenn du dich von einem neuen Gerät aus anmeldest." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Fahre unten fort, um Verifizierungs-E-Mails von Bitwarden zu erhalten, wenn du dich von einem neuen Gerät aus anmeldest." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Wenn der neue Geräte-Anmeldeschutz ausgeschaltet ist, kann jeder mit deinem Master-Passwort von jedem Gerät aus auf dein Konto zugreifen. Um dein Konto ohne Verifizierungs-E-Mails zu schützen, richte die Zwei-Faktor-Authentifizierung ein." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Änderungen am neuen Geräte-Anmeldeschutz gespeichert" + }, "sessionsDeauthorized": { "message": "Alle Sitzungen wurden abgemeldet" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Verwalten" }, - "canManage": { - "message": "Darf verwalten" + "manageCollection": { + "message": "Sammlung verwalten" + }, + "viewItems": { + "message": "Einträge anzeigen" + }, + "viewItemsHidePass": { + "message": "Einträge anzeigen, versteckte Passwörter" + }, + "editItems": { + "message": "Einträge bearbeiten" + }, + "editItemsHidePass": { + "message": "Einträge bearbeiten, versteckte Passwörter" }, "disable": { "message": "Deaktivieren" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Es gab ein Problem beim Lesen des Sicherheitsschlüssels, bitte versuche es erneut." }, - "twoFactorWebAuthnWarning": { - "message": "Aufgrund von Plattformbeschränkungen kann WebAuthn nicht in allen Bitwarden-Anwendungen verwendet werden. Du solltest einen anderen Zwei-Faktor-Authentifizierungsanbieter aktivieren, damit du auf dein Konto zugreifen kannst, wenn WebAuthn nicht verwendet werden kann. Unterstützte Plattformen:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web-Tresor und Browser-Erweiterungen auf einem Desktop/Laptop mit einem WebAuthn-fähigen Browser (Chrome, Opera, Vivaldi oder Firefox mit aktiviertem FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Aufgrund von Plattformbeschränkungen kann WebAuthn nicht in allen Bitwarden-Anwendungen verwendet werden. Du solltest einen anderen Zwei-Faktor-Authentifizierungsanbieter einrichten, damit du auf dein Konto zugreifen kannst, wenn WebAuthn nicht verwendet werden kann." }, "twoFactorRecoveryYourCode": { "message": "Ihr Wiederherstellungsschlüssel für die Zwei-Faktor-Anmeldung in Bitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Du hast noch eine Einladung übrig." }, + "inviteZeroEmailDesc": { + "message": "Du hast 0 Einladungen übrig." + }, "userUsingTwoStep": { "message": "Dieser Benutzer hat sein Konto mit einer Zwei-Faktor-Authentifizierung geschützt." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "SSO-Verknüpfung aufgehoben." + }, "unlinkedSsoUser": { "message": "SSO-Verknüpfung für Benutzer $ID$ aufgehoben.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Vertrauenswürdig" }, + "needsApproval": { + "message": "Benötigt Genehmigung" + }, + "areYouTryingtoLogin": { + "message": "Versuchst du dich anzumelden?" + }, + "logInAttemptBy": { + "message": "Anmeldeversuch von $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Gerätetyp" + }, + "ipAddress": { + "message": "IP-Adresse" + }, + "confirmLogIn": { + "message": "Anmeldung genehmigen" + }, + "denyLogIn": { + "message": "Anmeldung ablehnen" + }, + "thisRequestIsNoLongerValid": { + "message": "Diese Anfrage ist nicht mehr gültig." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Anmeldung von $EMAIL$ auf $DEVICE$ bestätigt", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Du hast einen Anmeldeversuch von einem anderen Gerät abgelehnt. Wenn du das wirklich warst, versuche dich erneut mit dem Gerät anzumelden." + }, + "loginRequestHasAlreadyExpired": { + "message": "Anmeldeanfrage ist bereits abgelaufen." + }, + "justNow": { + "message": "Gerade eben" + }, + "requestedXMinutesAgo": { + "message": "Vor $MINUTES$ Minuten angefordert", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Konto wird erstellt bei" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Mindestanforderungen für den Passwort-Generator festlegen." }, - "passwordGeneratorPolicyInEffect": { - "message": "Eine oder mehrere Organisationsrichtlinien beeinflussen deine Generator-Einstellungen." - }, "masterPasswordPolicyInEffect": { "message": "Eine oder mehrere Organisationsrichtlinien erfordern, dass dein Master-Passwort die folgenden Anforderungen erfüllt:" }, @@ -5686,11 +5849,11 @@ "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." }, "contactCSToAvoidDataLossPart1": { - "message": "Kontaktiere das Kundenerfolgsteam", + "message": "Kontaktiere unser Customer Success Team", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "um zusätzlichen Datenverlust zu vermeiden.", + "message": ", um zusätzlichen Datenverlust zu vermeiden.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Was möchtest du generieren?" - }, - "passwordType": { - "message": "Passworttyp" - }, - "regenerateUsername": { - "message": "Benutzername neu generieren" - }, "generateUsername": { "message": "Benutzername generieren" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Benutzernamenstyp" - }, "plusAddressedEmail": { "message": "Plus-adressierte E-Mail-Adresse", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Verwende den konfigurierten Catch-All-Posteingang deiner Domain." }, + "useThisEmail": { + "message": "Diese E-Mail-Adresse verwenden" + }, "random": { "message": "Zufällig", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API-Zugriffstoken" - }, "deviceVerification": { "message": "Geräteverifizierung" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Keine Sammlung" }, - "canView": { - "message": "Darf anzeigen" - }, - "canViewExceptPass": { - "message": "Darf anzeigen, Passwörter ausgenommen" - }, - "canEdit": { - "message": "Darf bearbeiten" - }, - "canEditExceptPass": { - "message": "Darf bearbeiten, Passwörter ausgenommen" - }, "noCollectionsAdded": { "message": "Keine Sammlungen hinzugefügt" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Vertrauenswürdige Geräte" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Einmal authentifiziert, entschlüsseln Mitglieder Tresordaten mit einem Schlüssel auf ihrem Gerät. Die Richtlinie für", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Mitglieder benötigen kein Master-Passwort beim Anmelden mit SSO. Das Master-Passwort wird durch einen auf dem Gerät gespeicherten Verschlüsselungsschlüssel ersetzt, wodurch das Gerät vertrauenswürdig ist. Dem erste Gerät, auf das ein Mitglied sein Konto erstellt und sich anmeldet, wird vertraut. Neue Geräte müssen von einem bestehenden vertrauenswürdigen Gerät oder einem Administrator genehmigt werden. Die Richtlinie für eine", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "einzelne Organisationen", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "einzelne Organisation", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": ", die Richtlinie zum", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "Verlangen einer SSO-Authentifizierung", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "und die Richtlinie für die", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "Kontowiederherstellungsverwaltung", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "mit automatischer Registrierung werden aktiviert, wenn diese Option verwendet wird.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "werden aktiviert, wenn diese Option verwendet wird.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Deine Organisationsberechtigungen wurden aktualisiert und verlangen, dass du ein Master-Passwort festlegen musst.", @@ -8275,16 +8414,16 @@ "message": "Anfrage genehmigen" }, "deviceApproved": { - "message": "\"Gerät\" genehmigt" + "message": "Gerät genehmigt" }, "deviceRemoved": { - "message": "\"Gerät\" entfernt" + "message": "Gerät entfernt" }, "removeDevice": { - "message": "\"Gerät\" entfernen" + "message": "Gerät entfernen" }, "removeDeviceConfirmation": { - "message": "Möchtest du das \"Gerät\" wirklich entfernen?" + "message": "Bist du sicher, das du dieses Gerät entfernen möchtest?" }, "noDeviceRequests": { "message": "Keine Geräteanfragen" @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Das Löschen von Sammlungen auf Eigentümer und Administratoren beschränken" }, + "limitItemDeletionDesc": { + "message": "Löschung von Einträgen auf Mitglieder mit der \"Darf verwalten\"-Berechtigung beschränken" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Besitzer und Administratoren können alle Sammlungen und Einträge verwalten" }, @@ -8544,9 +8686,6 @@ "message": "Selbst gehostete Server-URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias-Domain" - }, "alreadyHaveAccount": { "message": "Hast du bereits ein Konto?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Du hast keinen Zugriff zur Verwaltung dieser Sammlung." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Fehlende \"Darf verwalten\"-Berechtigungen" + "grantManageCollectionWarningTitle": { + "message": "Berechtigungen zur Verwaltung von Sammlungen fehlen" }, - "grantAddAccessCollectionWarning": { - "message": "\"Darf verwalten\"-Berechtigungen gewähren, um die vollständige Sammlungsverwaltung einschließlich der Löschung von Sammlungen zu ermöglichen." + "grantManageCollectionWarning": { + "message": "Berechtigungen zur Verwaltung von Sammlungen gewähren, um die vollständige Verwaltung von Sammlungen, einschließlich der Löschung von Sammlungen, zu erlauben." }, "grantCollectionAccess": { "message": "Gewähre Gruppen oder Mitgliedern Zugriff auf diese Sammlung." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "Monat pro Mitglied" }, + "monthPerMemberBilledAnnually": { + "message": "Monat pro Mitglied jährlich in Rechnung gestellt" + }, "seats": { "message": "Benutzerplätze" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Schlüsselalgorithmus" }, + "sshPrivateKey": { + "message": "Privater Schlüssel" + }, + "sshPublicKey": { + "message": "Öffentlicher Schlüssel" + }, + "sshFingerprint": { + "message": "Fingerabdruck" + }, "sshKeyFingerprint": { "message": "Fingerabdruck" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Beschreibungscode" }, + "cannotRemoveViewOnlyCollections": { + "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Wichtiger Hinweis" }, @@ -9986,13 +10146,13 @@ "message": "Mitglieder entfernen" }, "devices": { - "message": "\"Geräte\"" + "message": "Geräte" }, "deviceListDescription": { - "message": "Dein Konto wurde bei jedem der folgenden \"Geräte\" eingeloggt. Wenn du ein \"Gerät\" nicht wiedererkennst, dann entferne es jetzt." + "message": "Dein Konto ist bei jedem der folgenden Geräte angemeldet. Wenn du ein Gerät nicht wiedererkennst, entferne es jetzt." }, "deviceListDescriptionTemp": { - "message": "Dein Konto wurde bei jedem der folgenden \"Geräte\" eingeloggt." + "message": "Dein Konto wurde bei jedem der unten aufgeführten Geräte angemeldet." }, "claimedDomains": { "message": "Beanspruchte Domains" @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Der Name der Organisation darf 50 Zeichen nicht überschreiten." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Dein Abonnement wird bald verlängert. Um einen ununterbrochenen Betrieb zu gewährleisten, kontaktiere $RESELLER$ um deine Verlängerung vor dem $RENEWAL_DATE$ zu bestätigen.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Eine Rechnung für dein Abonnement wurde am $ISSUED_DATE$ ausgestellt. Um einen ununterbrochenen Betrieb zu gewährleisten, kontaktiere $RESELLER$, um deine Verlängerung vor dem $DUE_DATE$ zu bestätigen.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Die Rechnung für dein Abonnement wurde nicht bezahlt. Um einen ununterbrochenen Betrieb zu gewährleisten, kontaktiere $RESELLER$, um deine Verlängerung vor dem $GRACE_PERIOD_END$ zu bestätigen.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administratoren haben nun die Möglichkeit, Mitgliedskonten, die zu einer beanspruchten Domain gehören, zu löschen." + }, + "deleteManagedUserWarningDesc": { + "message": "Diese Aktion löscht das Mitgliedskonto, einschließlich aller Einträge in seinem Tresor. Dies ersetzt die vorherige Entfernen-Aktion." + }, + "deleteManagedUserWarning": { + "message": "Löschen ist eine neue Aktion!" + }, + "seatsRemaining": { + "message": "Du hast $REMAINING$ Plätze von $TOTAL$ dieser Organisation zugewiesenen Plätzen verfügbar. Kontaktiere deinen Anbieter, um dein Abonnement zu verwalten.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Vorhandene Organisation" + }, + "selectOrganizationProviderPortal": { + "message": "Wähle eine Organisation aus, um sie deinem Anbieterportal hinzuzufügen." + }, + "noOrganizations": { + "message": "Keine Sammlungen vorhanden" + }, + "yourProviderSubscriptionCredit": { + "message": "Dein Anbieterabonnement erhält ein Guthaben für jede verbleibende Zeit im Abonnement der Organisation." + }, + "doYouWantToAddThisOrg": { + "message": "Möchtest du diese Organisation zu $PROVIDER$ hinzufügen?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Vorhandene Organisation hinzugefügt" + }, + "assignedExceedsAvailable": { + "message": "Die zugewiesenen Plätze überschreiten die verfügbaren Plätze." } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index adfb86665ab3..ea5f11e91626 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -3,22 +3,22 @@ "message": "Όλες οι εφαρμογές" }, "criticalApplications": { - "message": "Critical applications" + "message": "Κρίσιμες εφαρμογές" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "Πληροφορίες Πρόσβασης" }, "riskInsights": { - "message": "Risk Insights" + "message": "Insights Κινδύνου" }, "passwordRisk": { - "message": "Password Risk" + "message": "Ρίσκος Κωδικού Πρόσβασης" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Ελέξτε τους κωδικούς πρόσβασης (αδύναμους, εκτεθειμένους ή επαναχρησιμοποιούμενους) σε όλες τις εφαρμογές. Επιλέξτε τις πιο κρίσιμες εφαρμογές σας για να δώσετε προτεραιότητα στις ενέργειες ασφαλείας για τους χρήστες σας ώστε να αντιμετωπίσουν τους εκτεθειμένους κωδικούς πρόσβασης." }, "dataLastUpdated": { - "message": "Data last updated: $DATE$", + "message": "Τελευταία ενημέρωση δεδομένων: $DATE$", "placeholders": { "date": { "content": "$1", @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Σύνολο μελών" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Σύνολο εφαρμογών" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Τι είδους στοιχείο είναι αυτό;" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Βασικός τομέας", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Όνομα αντικειμένου" }, - "cannotRemoveViewOnlyCollections": { - "message": "Δεν μπορείτε να αφαιρέσετε συλλογές που έχουν μόνο δικαιώματα Προβολής: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "πχ.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Επαληθεύστε την ταυτότητά σας" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Η σύνδεση ξεκίνησε" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Υποβολή" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Έκδοση $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Επαναδημιουργία Κωδικού" - }, "length": { "message": "Μήκος" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Επικίνδυνη Ζώνη" }, - "dangerZoneDesc": { - "message": "Προσοχή, αυτές οι ενέργειες είναι μη αναστρέψιμες!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Κατάργηση Εξουσιοδότησης Συνεδριών" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία και θα σας ζητήσει να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να παραμείνουν ενεργοποιημένες για έως και μία ώρα." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Η Ανακληθεί η Πρόσβαση από Όλες τις Συνεδρίες" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Διαχείριση" }, - "canManage": { - "message": "Δυνατότητα διαχείρισης" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Απενεργοποίηση" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Παρουσιάστηκε πρόβλημα κατά την ανάγνωση του κλειδιού ασφαλείας. Προσπάθησε ξανά." }, - "twoFactorWebAuthnWarning": { - "message": "Λόγω περιορισμών της πλατφόρμας, το WebAuthn δεν μπορεί να χρησιμοποιηθεί σε όλες τις εφαρμογές Bitwarden. Θα πρέπει να ενεργοποιήσετε έναν άλλο πάροχο σύνδεσης δύο βημάτων, ώστε να έχετε πρόσβαση στο λογαριασμό σας όταν δεν μπορεί να χρησιμοποιηθεί το WebAuthn. Υποστηριζόμενες πλατφόρμες:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault και επεκτάσεις browser σε έναν σταθερό / φορητό υπολογιστή με ένα πρόγραμμα περιήγησης WebAuthn (Chrome, Opera, Vivaldi, ή Firefox με FIDO U2F ενεργοποιημένο)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Ο Bitwarden κωδικός ανάκτησης, εισόδου δύο βημάτων" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Αυτός ο χρήστης χρησιμοποιεί τρόπο σύνδεσης δύο βημάτων για να προστατεύσει το λογαριασμό του." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Αποσυνδεδεμένο SSO για το χρήστη $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Δημιουργία λογαριασμού στο" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Ελάχιστες απαιτήσεις διαμόρφωσης γεννήτριας κωδικών." }, - "passwordGeneratorPolicyInEffect": { - "message": "Μία ή περισσότερες πολιτικές του οργανισμού επηρεάζουν τις ρυθμίσεις της γεννήτριας." - }, "masterPasswordPolicyInEffect": { "message": "Σε μία ή περισσότερες πολιτικές του οργανισμού απαιτείται ο κύριος κωδικός να πληρεί τις ακόλουθες απαιτήσεις:" }, @@ -6554,15 +6717,6 @@ "message": "Γεννήτρια", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Τι θα θέλατε να δημιουργήσετε;" - }, - "passwordType": { - "message": "Τύπος Κωδικού" - }, - "regenerateUsername": { - "message": "Επαναδημιουργία Ονόματος Χρήστη" - }, "generateUsername": { "message": "Δημιουργία Ονόματος Χρήστη" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Τύπος Ονόματος Χρήστη" - }, "plusAddressedEmail": { "message": "Συν Διεύθυνση Email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Χρησιμοποιήστε τα διαμορφωμένα εισερχόμενα catch-all του domain σας." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Τυχαίο", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Όνομα κεντρικού υπολογιστή", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Διακριτικό πρόσβασης API" - }, "deviceVerification": { "message": "Επαλήθευση συσκευής" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Καμία συλλογή" }, - "canView": { - "message": "Δυνατότητα προβολής" - }, - "canViewExceptPass": { - "message": "Δυνατότητα προβολής, εκτός των κωδικών πρόσβασης" - }, - "canEdit": { - "message": "Δυνατότητα επεξεργασίας" - }, - "canEditExceptPass": { - "message": "Δυνατότητα επεξεργασίας, εκτός των κωδικών πρόσβασης" - }, "noCollectionsAdded": { "message": "Δεν προστέθηκαν συλλογές" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Έμπιστες συσκευές" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "Απαιτείται SSO", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Οι ιδιοκτήτες και οι διαχειριστές μπορούν να διαχειριστούν όλες τις συλλογές και τα στοιχεία" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Ψευδώνυμο τομέα" - }, "alreadyHaveAccount": { "message": "Έχετε ήδη λογαριασμό;" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Δεν έχετε πρόσβαση για να διαχειριστείτε αυτήν τη συλλογή." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "μήνας ανά μέλος" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Θέσεις" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "Δεν μπορείτε να αφαιρέσετε συλλογές που έχουν μόνο δικαιώματα Προβολής: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 15c5a7fcf6ce..0f48595f09bc 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -176,6 +176,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -479,6 +485,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -743,15 +761,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1182,6 +1191,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1191,6 +1206,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1380,12 +1398,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1673,9 +1718,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1780,7 +1822,7 @@ }, "requestPending": { "message": "Request pending" - }, + }, "logBackInOthersToo": { "message": "Please log back in. If you are using other Bitwarden applications log out and back in to those as well." }, @@ -1848,12 +1890,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1863,6 +1899,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message":"New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message":"Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message":"Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message":"Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message":"Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message":"With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2164,8 +2221,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2362,11 +2431,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3789,6 +3855,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -4701,9 +4770,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6672,15 +6738,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6721,9 +6778,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6737,6 +6791,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6940,9 +6997,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7654,18 +7708,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8613,6 +8655,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8662,9 +8707,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8726,11 +8768,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9295,6 +9337,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9815,6 +9860,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -10068,6 +10122,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10258,5 +10321,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification" : { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index a2002039ab99..24aa0b675ab4 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "e.g.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorise sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails Bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have Bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorised" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organisation policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organisation policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device from where a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organisation", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrolment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organisation permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Organisation name cannot exceed 50 characters." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organisation. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organisation" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organisation to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organisations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organisation's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organisation to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organisation" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 2564812802a7..f4e4756b3933 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "e.g.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorise sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if enabled. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails Bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have Bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorised" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Disable" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should enable another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn enabled browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F enabled)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set minimum requirements for password generator configuration." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organisation policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organisation policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device from where a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organisation", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organisation permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Organisation name cannot exceed 50 characters." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organisation. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organisation" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organisation to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organisations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organisation's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organisation to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organisation" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 21eba5a4a994..a864ee995903 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Kiuspeca estas ĉi tiu ero?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Redakti dosierujon" }, + "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Baza domajno", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ekz.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Sendu" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Versio $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regeneri Pasvorton" - }, "length": { "message": "Longo" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danĝera Zono" }, - "dangerZoneDesc": { - "message": "Atentu, ĉi tiuj agoj ne estas reigeblaj!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Senrajtigi Sesiojn" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Se vi daŭrigos vian adiaŭadon de la nuna seanco, necesos vin saluti denove. Oni ankaŭ demandos de vi du-faktoran aŭtentigon, se tiu elekteblo estas ebligita. La seancoj aktivaj sur aliaj aparatoj povas resti daŭre aktivaj ankoraŭ unu horon." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Ĉiuj Sesioj Neaŭtorizitaj" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Administri" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Neebligi" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Via kodo de senpaneigo de du-ŝtupa saluto de Bitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Ĉi tiu uzanto uzas du-paŝan ensaluton por protekti sian konton." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Agordi minimumajn postulojn por agordi pasvortan generilon." }, - "passwordGeneratorPolicyInEffect": { - "message": "Unu aŭ pluraj organizaj politikoj influas viajn generatorajn agordojn." - }, "masterPasswordPolicyInEffect": { "message": "Unu aŭ pluraj organizaj politikoj postulas vian ĉefan pasvorton por plenumi la jenajn postulojn:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Hazarda", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Domajna nomo", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 1393fe59b274..0eaf89762658 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "¿Qué tipo de elemento es este?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Editar carpeta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Dominio base", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Nombre del elemento" }, - "cannotRemoveViewOnlyCollections": { - "message": "No puedes eliminar colecciones con permisos de solo visualización: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ej.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verifica tu identidad" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Inicio de sesión en proceso" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Enviar" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Se ha enviado una notificación a tu dispositivo." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Versión $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerar contraseña" - }, "length": { "message": "Longitud" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Zona peligrosa" }, - "dangerZoneDesc": { - "message": "¡Cuidado, estas acciones no son reversibles!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Desautorizar sesiones" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceder también cerrará tu sesión actual, requiriendo que vuelvas a identificarte. También se te pedirá nuevamente tu autenticación en dos pasos en caso de que la tengas habilitada. Las sesiones activas en otros dispositivos pueden mantenerse activas hasta una hora más." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Desautorizadas todas las sesiones" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Gestionar" }, - "canManage": { - "message": "Puede gestionar" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Desactivar" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Hubo un problema al leer la llave de seguridad. Inténtalo de nuevo." }, - "twoFactorWebAuthnWarning": { - "message": "Debido a las limitaciones de la plataforma, WebAuthn no puede ser utilizado en todas las aplicaciones de Bitwarden. Debería habilitar otro proveedor de inicio de sesión en dos-pasos para que pueda acceder a su cuenta cuando WebAuthn no pueda ser usado. Plataformas soportadas:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Caja fuerte web y extensiones de navegador en un escritorio/portátil con un navegador WebAuthn habilitado (Chrome, Opera, Vivaldi o Firefox con FIDO U2F habilitado)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Tu código de recuperación de inicio de sesión de dos pasos de Bitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Este usuario está usando autenticación de dos pasos para proteger su cuenta." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "SSO desvinculado para el usuario $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creando una cuenta en" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Establecer requisitos mínimos para la configuración del generador de contraseñas." }, - "passwordGeneratorPolicyInEffect": { - "message": "Una o más políticas de la organización están afectando su configuración del generador." - }, "masterPasswordPolicyInEffect": { "message": "Una o más políticas de la organización requieren que su contraseña maestra cumpla con los siguientes requisitos:" }, @@ -6554,15 +6717,6 @@ "message": "Generador", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "¿Qué desea generar?" - }, - "passwordType": { - "message": "Tipo de contraseña" - }, - "regenerateUsername": { - "message": "Regenerar nombre de usuario" - }, "generateUsername": { "message": "Generar nombre de usuario" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Tipo de nombre de usuario" - }, "plusAddressedEmail": { "message": "Correo electrónico más dirigido", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Utiliza la bandeja de entrada global configurada de tu dominio." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Aleatorio", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Nombre del host", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token de acceso API" - }, "deviceVerification": { "message": "Verificación del dispositivo" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Sin colecciones" }, - "canView": { - "message": "Puede ver" - }, - "canViewExceptPass": { - "message": "Puede ver, excepto contraseñas" - }, - "canEdit": { - "message": "Puede editar" - }, - "canEditExceptPass": { - "message": "Puede editar, excepto contraseñas" - }, "noCollectionsAdded": { "message": "No hay colecciones añadidas" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Dispositivos de confianza" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "organización única", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "política,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO requerido", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "política, y", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "administración de recuperación de cuenta", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Propietarios y administradores pueden gestionar todas las colecciones y elementos" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Dominio alias" - }, "alreadyHaveAccount": { "message": "¿Ya tienes una cuenta?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "No puedes eliminar colecciones con permisos de solo visualización: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 9122e4b87783..ede248d2b934 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Mis tüüpi kirje see on?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Muuda kausta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Domeen", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Kirje nimi" }, - "cannotRemoveViewOnlyCollections": { - "message": "Sa ei saa eemaldada neid kogumikke ainult vaatamisloaga: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "nt.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Kinnitage oma Identiteet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Sisselogimine käivitatud" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Kinnita" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Sinu seadmesse saadeti teavitus." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Versioon $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Genereeri parool uuesti" - }, "length": { "message": "Pikkus" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Ohtlik tsoon" }, - "dangerZoneDesc": { - "message": "Ettevaatust, neid toiminguid ei saa tagasi võtta!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Sessioonide tühistamine" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Jätkatest logitakse sind ka käimasolevast sessioonist välja, mistõttu pead kontosse uuesti sisse logima. Lisaks võidakse küsida kaheastmelist kinnitust, kui see on sisse lülitatud. Teised kontoga ühendatud seadmed võivad jääda sisselogituks kuni üheks tunniks." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Kõikidest seadmetest on välja logitud" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Haldus" }, - "canManage": { - "message": "Saab muuta" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Keela" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Turvavõtme lugemisel tekkis tõrge. Proovi uuesti." }, - "twoFactorWebAuthnWarning": { - "message": "Mõnede platvormi piirangute tõttu ei saa WebAuthn'i kõikide Bitwardeni rakendustega kasutada. Võiksid kaaluda teise kaheastmelise kinnitamise aktiveerimist olukordadeks, kus WebAuthn'i ei saa kasutada. Toetatud platvormid:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Veebihoidla- ja brauseri laiendused laua- ja sülearvutis, kus on WebAuthn toega brauser (Chrome, Opera, Vivaldi või Firefox, kus on FIDO U2F sisse lülitatud)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Bitwardeni kaheastmelise logimise varukood" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Sellel kasutajal on kaheastmeline kinnitamine sisse lülitatud." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Kasutaja $ID$ SSO on eemaldatud.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Konto loomise asukoht" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Määra parooli genereerija konfiguratsiooni tingimused." }, - "passwordGeneratorPolicyInEffect": { - "message": "Organisatsiooni seaded mõjutavad parooli genereerija sätteid." - }, "masterPasswordPolicyInEffect": { "message": "Üks või enam organisatsiooni eeskirja nõuavad, et ülemparool vastaks nendele nõudmistele:" }, @@ -6554,15 +6717,6 @@ "message": "Genereerija", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Mida sa soovid genereerida?" - }, - "passwordType": { - "message": "Parooli tüüp" - }, - "regenerateUsername": { - "message": "Genereeri kasutajanimi uuesti" - }, "generateUsername": { "message": "Genereeri kasutajanimi" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Kasutajanime tüüp" - }, "plusAddressedEmail": { "message": "Plussiga e-posti aadress", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Kasuta domeenipõhist kogumisaadressi." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Juhuslik", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hosti nimi", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API võti" - }, "deviceVerification": { "message": "Seadme kinnitamine" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domeen" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "Sa ei saa eemaldada neid kogumikke ainult vaatamisloaga: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 576d8207aaee..0b230da15452 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Zein elementu mota da hau?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Oinarrizko domeinua", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "adb.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Saioa hastea martxan da" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Bidali" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Jakinarazpen bat bidali da zure gailura." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "$VERSION_NUMBER$ bertsioa", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Berrezarri pasahitza" - }, "length": { "message": "Luzera" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Eremu arriskutsua" }, - "dangerZoneDesc": { - "message": "Kontuz, ekintza hauek ez dira itzulgarriak!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Baimena kendu saio hasierei" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Jarraitzeak uneko saioa itxiko du eta berriro saioa hasteko eskatuko zaizu. Gaituta badago, berriz ere bi urratseko saioa hasiera eskatuko zaizu. Beste gailu batzuetako saio aktiboek ordubete iraun dezakete aktibo." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Saio guztiei baimena kendua" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Kudeatu" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Desgaitu" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Arazo bat egon da segurtasun-gakoa irakurtzean. Saiatu berriro mesedez." }, - "twoFactorWebAuthnWarning": { - "message": "Plataformaren mugak direla eta, WebAuthn ezin da erabili Bitwarden-en aplikazio guztietan. Bi urratseko saio hasierako beste hornitzaile bat gaitu beharko duzu, WebAuthn erabili ezin denean zure kontuan sartzeko. Plataforma bateragarriak:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Webguneko kutxa gotorra eta nabigatzailearen gehiagarriak mahaigain/ordenagailu eramangarri batean, WebAuthn nabigatzaile gaitu batekin (Chrome, Opera, Vivaldi edo Firefox, FIDO U2F gaitua duena)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Zure Bitwardeneko bi urratseko saio hasierako berreskuratze-kodea" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Erabiltzaile hau bi urratseko saio hasiera erabiltzen ari da bere kontua babesteko." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "$ID$ erabiltzailearentzako SSO lotura kendua.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Ezarri pasahitz sortzailearen gutxieneko baldintzak." }, - "passwordGeneratorPolicyInEffect": { - "message": "Erakundeko politika batek edo gehiagok sortzailearen konfigurazioari eragiten diote." - }, "masterPasswordPolicyInEffect": { "message": "Erakundeko politika batek edo gehiagok pasahitz nagusia behar dute baldintza hauek betetzeko:" }, @@ -6554,15 +6717,6 @@ "message": "Sortzailea", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Zer sortu nahi duzu?" - }, - "passwordType": { - "message": "Pasahitz mota" - }, - "regenerateUsername": { - "message": "Berrezarri erabiltzaile izena" - }, "generateUsername": { "message": "Sortu erabiltzaile izena" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Erabiltzaile izen mota" - }, "plusAddressedEmail": { "message": "Atzizkidun emaila", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Erabili zure domeinuan konfiguratutako sarrerako ontzia." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Ausazkoa", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Ostalariaren izena", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token sarbide API-a" - }, "deviceVerification": { "message": "Gailu egiaztapena" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Ikus dezake" - }, - "canViewExceptPass": { - "message": "Ikus dezake, pasahitza izan ezik" - }, - "canEdit": { - "message": "Editatu dezake" - }, - "canEditExceptPass": { - "message": "Editatu dezake, pasahitza izan ezik" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 589a019973f2..cfcf12c79071 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "این چه نوع موردی است؟" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "دامنه پایه", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "مثال.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "ورود به سیستم آغاز شد" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "ثبت" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "یک اعلان به دستگاه شما ارسال شده است." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "نسخه $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "تولید مجدد کلمه عبور" - }, "length": { "message": "طول" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "منطقه‌ی خطر" }, - "dangerZoneDesc": { - "message": "مراقب باشید، این اقدامات قابل برگشت نیستند!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "لغو مجوز نشست‌ها" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "ادامه دادن همچنین شما را از نشست فعلی خود خارج می‌کند و باید دوباره وارد سیستم شوید. در صورت راه اندازی مجدداً از شما خواسته می‌شود تا دوباره به سیستم دو مرحله ای بپردازید. جلسات فعال در دستگاه‌های دیگر ممکن است تا یک ساعت فعال بمانند." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "همه نشست‌ها غیرمجاز است" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "مدیریت" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "خاموش کردن" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "مشکلی در خواندن کلید امنیتی وجود داشت. دوباره امتحان کنید." }, - "twoFactorWebAuthnWarning": { - "message": "به دلیل محدودیت‌های پلتفرم، WebAuthn نمی‌تواند در همه برنامه‌های Bitwarden استفاده شود. باید یک ارائه‌دهنده‌ی ورود دو مرحله‌ای دیگر راه‌اندازی کنید تا زمانی که WebAuthn قابل استفاده نیست، بتوانید به حساب خود دسترسی داشته باشید. پلتفرم های پشتیبانی شده:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "گاوصندوق وب و برنامه‌های افزودنی مرورگر روی دسکتاپ/لپ‌تاپ با مرورگر پشتیبانی‌شده از WebAuthn (Chrome، Opera، Vivaldi یا Firefox با FIDO U2F روشن)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "کد بازیابی ورود دو مرحله ای Bitwarden شما" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "این کاربر از ورود دو مرحله ای برای محافظت از حساب خود استفاده می‌کند." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "SSO برای کاربر $ID$ پیوند نخورده است.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "الزامات را برای تولید کننده کلمه عبور تنظیم کنید." }, - "passwordGeneratorPolicyInEffect": { - "message": "یک یا چند سیاست سازمان بر تنظیمات تولید کننده شما تأثیر می‌گذارد." - }, "masterPasswordPolicyInEffect": { "message": "یک یا چند سیاست سازمانی برای تأمین شرایط زیر به کلمه عبور اصلی شما احتیاج دارد:" }, @@ -6554,15 +6717,6 @@ "message": "تولید کننده", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "چه چیزی دوست دارید تولید کنید؟" - }, - "passwordType": { - "message": "نوع کلمه عبور" - }, - "regenerateUsername": { - "message": "ایجاد مجدد نام کاربری" - }, "generateUsername": { "message": "ایجاد نام کاربری" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "نوع نام کاربری" - }, "plusAddressedEmail": { "message": "به علاوه نشانی ایمیل داده شده", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "از صندوق ورودی پیکربندی شده دامنه خود استفاده کنید." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "تصادفی", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "نام میزبان", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "توکن دسترسی API" - }, "deviceVerification": { "message": "تأیید دستگاه" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "مجموعه ای وجود ندارد" }, - "canView": { - "message": "می‌تواند مشاهده کند" - }, - "canViewExceptPass": { - "message": "قابل مشاهده، به غیر از کلمه‌های عبور" - }, - "canEdit": { - "message": "می‌تواند ویرایش کند" - }, - "canEditExceptPass": { - "message": "قابل ویرایش، به غیر از کلمه‌های عبور" - }, "noCollectionsAdded": { "message": "مجموعه ای اضافه نشد" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "دستگاه‌های مورد اعتماد" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "پس از احراز هویت، اعضا با استفاده از کلید ذخیره شده در دستگاه خود، داده‌های گاوصندوق را رمزگشایی می‌کنند.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "سازمان واحد", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "سیاست‌ها", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO الزامی است", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "سیاست‌ها و", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "مدیریت بازیابی حساب کاربری", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "سیاست‌ها با ثبت نام خودکار زمانی که از این گزینه استفاده می‌شود روشن می‌شود.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "دامنه مستعار" - }, "alreadyHaveAccount": { "message": "پیشتر حساب کاربری داشته اید؟" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index b6825bed1625..00d56fdb1c1e 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Riskialttiit jäsenet" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Jäseniä yhteensä" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Sovelluksia yhteensä" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Minkä tyyppinen kohde tämä on?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Muokkaa kansiota" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Pääverkkotunnus", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Kohteen nimi" }, - "cannotRemoveViewOnlyCollections": { - "message": "Et voi poistaa kokoelmia, joihin sinulla on vain tarkasteluoikeus: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "esim.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Vahvista henkilöllisyytesi" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Kirjautuminen aloitettu" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Jatka" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Laitteellesi on lähetetty ilmoitus." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Laitteeseesi lähetettiin ilmoitus" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Versio $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Vältä epäselviä merkkejä", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Luo uusi salasana" - }, "length": { "message": "Pituus" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Vaaravyöhyke" }, - "dangerZoneDesc": { - "message": "Ole varovainen! Näitä toimintoja ei ole mahdollista kumota!" - }, - "dangerZoneDescSingular": { - "message": "Ole varoivainen. Tätä ei ole mahdollista perua!" - }, "deauthorizeSessions": { "message": "Mitätöi kaikki istunnot" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Jatkaminen uloskirjaa myös nykyisen istunnon pakottaen uudelleenkirjautumisen sekä kaksivaiheinen kirjautumisen, jos se on määritetty. Muiden laitteiden aktiiviset istunnot saattavat toimia vielä tunnin ajan." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Kaikki istunnot mitätöitiin" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Hallitse" }, - "canManage": { - "message": "Voi hallita" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Poista käytöstä" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Suojausavainta luettaessa havaittiin ongelma. Yritä uudelleen." }, - "twoFactorWebAuthnWarning": { - "message": "Alustakohtaisten rajoitusten vuoksi WebAuthn-todennuslaiteet eivät ole käytettävissä kaikissa Bitwarden-sovelluksissa. Sinun tulisi määrittää eri kaksivaiheisen kirjautumisen todentaja, jotta pääset tilillesi myös silloin kun WebAuthn-laitteen käyttö ei ole mahdollista. Tuetut alustat:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Verkkoholvi ja selainlaajennukset pöytäkoneissa/kannettavissa, joissa on WebAuthn-tekniikkaa tukeva selain (Chrome, Opera, Vivaldi tai Firefox FIDO U2F käyttöön otettuna)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Bitwardenin kaksivaiheisen kirjautumisen palautuskoodisi" @@ -3279,7 +3378,10 @@ } }, "inviteSingleEmailDesc": { - "message": "You have 1 invite remaining." + "message": "Sinulla on 1 kutsu jäljellä." + }, + "inviteZeroEmailDesc": { + "message": "Sinulla on 0 kutsua jäljellä." }, "userUsingTwoStep": { "message": "Käyttäjä on suojannut tilinsä kaksivaiheisella kirjautumisella." @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Poisti kertakirjautumisen käyttäjältä \"$ID$\".", "placeholders": { @@ -3781,13 +3886,74 @@ "message": "Laite" }, "loginStatus": { - "message": "Login status" + "message": "Kirjautumisen tila" }, "firstLogin": { - "message": "First login" + "message": "Ensimmäinen kirjautuminen" }, "trusted": { - "message": "Trusted" + "message": "Luotettu" + }, + "needsApproval": { + "message": "Vaatii hyväksynnän" + }, + "areYouTryingtoLogin": { + "message": "Yritätkö kirjautua sisään?" + }, + "logInAttemptBy": { + "message": "Kirjautumisyritys osoitteella $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Juuri nyt" + }, + "requestedXMinutesAgo": { + "message": "Pyydetty $MINUTES$ minuuttia sitten", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } }, "creatingAccountOn": { "message": "Luodaan tili palvelimelle" @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Aseta salasanageneraattorin vaatimukset." }, - "passwordGeneratorPolicyInEffect": { - "message": "Yksi tai useampi organisaatiokäytäntö vaikuttaa generaattorisi asetuksiin." - }, "masterPasswordPolicyInEffect": { "message": "Yksi tai useampi organisaatiokäytäntö edellyttää, että pääsalasanasi täyttää seuraavat vaatimukset:" }, @@ -5686,11 +5849,11 @@ "message": "Bitwarden could not decrypt the vault item(s) listed below." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Ota yhteyttä asiakkaaseen", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "lisätietojen menettämisen välttämiseksi.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -6554,15 +6717,6 @@ "message": "Generaattori", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Mitä haluat luoda?" - }, - "passwordType": { - "message": "Salasanan tyyppi" - }, - "regenerateUsername": { - "message": "Luo uusi käyttäjätunnus" - }, "generateUsername": { "message": "Luo käyttäjätunnus" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Käyttäjätunnuksen tyyppi" - }, "plusAddressedEmail": { "message": "Plus+merkkinen sähköposti", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Käytä verkkotunnuksesi catch-all-postilaatikkoa." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Satunnainen", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Isäntä", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API-käyttötunniste" - }, "deviceVerification": { "message": "Laitevahvistus" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Ei kokoelmaa" }, - "canView": { - "message": "Voi tarkastella" - }, - "canViewExceptPass": { - "message": "Voi tarkastella (ei salasanoja)" - }, - "canEdit": { - "message": "Voi muokata" - }, - "canEditExceptPass": { - "message": "Voi muokata (ei salasanoja)" - }, "noCollectionsAdded": { "message": "Kokoelmia ei ole lisätty" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Luotetut laitteet" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Kun jäsenet ovat tunnistautuneet, he voivat purkaa holvin salauksen omalla laitteellaan säilytettävällä avaimella.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "\"Yksittäinen organisaatio\"", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "ja", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "\"Kertakirjautuminen vaaditaan\"", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "-käytännöt sekä", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "\"Tilien palautusavun hallinta\"", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "-käytäntö automaattisella liitoksella otetaan käyttöön tämän valinnan käyttöönoton yhteydessä.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Organisaatiosi käyttöoikeuksia muutettiin ja tämän seurauksena sinun on asetettava pääsalasana.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Rajoita kokoelmien poisto omistajille ja ylläpitäjille" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Omistajat ja ylläpitäjät voivat hallita kaikkia kokoelmia ja kohteita" }, @@ -8544,9 +8686,6 @@ "message": "Itse ylläpidetyn palvelimen URL-osoite", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Aliaksen verkkotunnus" - }, "alreadyHaveAccount": { "message": "Onko sinulla jo tili?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Oikeutesi eivät riitä kokoelman hallintaan." }, - "grantAddAccessCollectionWarningTitle": { - "message": "\"Voi hallita\" -käyttöoikeus puuttuu" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Myönnä ”Voi hallita” -oikeus, joka mahdollistaa kokoelman täydellisen hallinnan, mukaan lukien sen poistamisen." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Myönnä ryhmille tai henkilöille kokoelman käyttöoikeus." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "kuukaudessa/jäsen" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Käyttäjäpaikat" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Avainalgoritmi" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Sormenjälki" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "Et voi poistaa kokoelmia, joihin sinulla on vain tarkasteluoikeus: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Tärkeä ilmoitus" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 9dcacdb44f8e..42b4882bf201 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Ano'ng uri ng item na ito?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Baguhin 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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "hal.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Sinimulan ang pag-log in" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Ipadala" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Nakapagpadala na ng notipikasyon sa device mo." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Bersyon $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Gumawa ng isa pang password" - }, "length": { "message": "Haba" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Mapanganib na lugar" }, - "dangerZoneDesc": { - "message": "Mag-ingat, wala nang bawian sa mga gagawin mo rito!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "I-deauthorize ang mga sesyon" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Mala-log out ka rin sa kasalukuyan mong sesyon kung tutuloy ka, at kakailanganin mong mag-log in ulit. Kakailanganin mo ring ulitin ang dalawang-hakbang na pag-log in kung naka-set up ito. Maaaring manatiling aktibo ang mga sesyon sa iba pang device nang hanggang isang oras." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Na-deauthorize lahat ng mga sesyon" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Pamahalaan" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Isara" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Nagkaproblema sa pagbabasa ng security key. Pakisubukan ulit." }, - "twoFactorWebAuthnWarning": { - "message": "Hindi magagamit sa lahat ng mga aplikasyon ng Bitwarden ang WebAuthn dahil sa mga limitasyon ng platform. Dapat mag-set up ka ng isa pang provider ng dalawang-hakbang na pag-log in para ma-access mo ang account mo sakaling hindi magamit ang WebAuthn. Mga suportadong platform:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault at mga ekstensyon pang-browser sa desktop/laptop na may browser na sinusuportahan ang WebAuthn (Chrome, Opera, Vivaldi, o Firefox na nakabukas ang FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Code pang-recover mo sa dalawang-hakbang na pag-log in sa Bitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Gumagamit ang user na ito ng dalawang hakbang na pag login upang maprotektahan ang kanilang account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Hindi naka-link na SSO para sa user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Magtakda ng mga kinakailangan para sa generator ng password." }, - "passwordGeneratorPolicyInEffect": { - "message": "Isang o higit pang patakaran ng organisasyon ay nakakaapekto sa iyong mga setting ng generator." - }, "masterPasswordPolicyInEffect": { "message": "Isang o higit pang mga patakaran ng organisasyon ay nangangailangan ng iyong master password upang matugunan ang sumusunod na kinakailangan:" }, @@ -6554,15 +6717,6 @@ "message": "Magmamana", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Ano ang nais mong bumuo?" - }, - "passwordType": { - "message": "Uri ng Password" - }, - "regenerateUsername": { - "message": "Muling bumuo ng username" - }, "generateUsername": { "message": "Lumikha ng username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Uri ng username" - }, "plusAddressedEmail": { "message": "Plus na naka-address na email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Gamitin ang naka configure na inbox ng catch all ng iyong domain." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Pangalan ng Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token ng Access sa API" - }, "deviceVerification": { "message": "Pag verify ng aparato" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Walang koleksyon" }, - "canView": { - "message": "Maaaring tingnan" - }, - "canViewExceptPass": { - "message": "Maaaring tingnan, maliban sa mga password" - }, - "canEdit": { - "message": "Maaaring i-edit" - }, - "canEditExceptPass": { - "message": "Maaaring mag edit, maliban sa mga password" - }, "noCollectionsAdded": { "message": "Walang idinagdag na mga koleksyon" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 933bfd8759f5..a5a55255c287 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Membres à risque" }, + "atRiskMembersWithCount": { + "message": "Membres à risque ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Applications à risque ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Ces membres se connectent à des applications avec des mots de passe faibles, exposés ou réutilisés." + }, + "atRiskApplicationsDescription": { + "message": "Ces applications ont des mots de passe faibles, exposés ou réutilisés." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Ces membres se connectent à $APPNAME$ avec des mots de passe faibles, exposés ou réutilisés.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total des membres" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total des applications" }, + "unmarkAsCriticalApp": { + "message": "Ne plus marquer comme application critique" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Marquage d'application critique retiré avec succès" + }, "whatTypeOfItem": { "message": "Quel type d'élément est-ce ?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Modifier le dossier" }, + "newFolder": { + "message": "Nouveau dossier" + }, + "folderName": { + "message": "Nom de dossier" + }, + "folderHintText": { + "message": "Créez un sous-dossier en ajoutant le nom du dossier parent suivi d'un \"/\". Par exemple : Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Êtes-vous sûr de vouloir supprimer définitivement ce dossier ?" + }, "baseDomain": { "message": "Domaine de base", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Nom de l’élément" }, - "cannotRemoveViewOnlyCollections": { - "message": "Vous ne pouvez pas supprimer des collections avec les autorisations d'affichage uniquement : $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Vérifiez votre Identité" }, + "weDontRecognizeThisDevice": { + "message": "Nous ne reconnaissons pas cet appareil. Entrez le code envoyé à votre courriel pour vérifier votre identité." + }, + "continueLoggingIn": { + "message": "Continuer à vous connecter" + }, "whatIsADevice": { "message": "Qu'est-ce qu'un appareil ?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Connexion initiée" }, + "logInRequestSent": { + "message": "Demande envoyée" + }, "submit": { "message": "Soumettre" }, @@ -1297,7 +1348,7 @@ "message": "Aucun élément à afficher." }, "noPermissionToViewAllCollectionItems": { - "message": "Vous n'avez pas la permission de voir tous les éléments de cette collection." + "message": "Vous n'avez pas l'autorisation d'afficher tous les éléments de cette collection." }, "youDoNotHavePermissions": { "message": "Vous n'avez pas les autorisations pour cette collection" @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Une notification a été envoyée à votre appareil." }, + "notificationSentDevicePart1": { + "message": "Déverrouillez Bitwarden sur votre appareil ou sur le " + }, + "areYouTryingToAccessYourAccount": { + "message": "Essayez-vous d'accéder à votre compte ?" + }, + "accessAttemptBy": { + "message": "Tentative d'accès par $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirmer l'accès" + }, + "denyAccess": { + "message": "Refuser l'accès" + }, + "notificationSentDeviceAnchor": { + "message": "application web" + }, + "notificationSentDevicePart2": { + "message": "Assurez-vous que la phrase d'empreinte correspond à celle ci-dessous avant d'approuver." + }, + "notificationSentDeviceComplete": { + "message": "Déverrouillez Bitwarden sur votre appareil. Assurez-vous que la phrase d'empreinte correspond à celle ci-dessous avant d'approuver." + }, "aNotificationWasSentToYourDevice": { "message": "Une notification a été envoyée à votre appareil" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Assurez-vous que votre compte est déverrouillé et que la phrase d'empreinte correspond à l'autre appareil" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Éviter les caractères ambigus", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Régénérer un mot de passe" - }, "length": { "message": "Longueur" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Zone de danger" }, - "dangerZoneDesc": { - "message": "Attention, ces actions sont irréversibles !" - }, - "dangerZoneDescSingular": { - "message": "Attention, cette action est irréversible!" - }, "deauthorizeSessions": { "message": "Révoquer les sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "En poursuivant, vous serez également déconnecté de votre session en cours, ce qui vous obligera à vous reconnecter. Vous serez également invité à vous reconnecter via l'authentification à deux facteurs, si elle est configurée. Les sessions actives sur d'autres appareils peuvent rester actives pendant encore une heure." }, + "newDeviceLoginProtection": { + "message": "Connexion à un nouvel appareil" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Désactivez la protection de connexion du nouvel appareil" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Activez la protection de connexion du nouvel appareil" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Procédez ci-dessous pour désactiver les courriels de vérification envoyés par Bitwarden lorsque vous vous connectez à partir d'un nouvel appareil." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Procédez ci-dessous pour que Bitwarden vous envoie des courriels de vérification lorsque vous vous connectez à partir d'un nouvel appareil." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Avec la protection de connexion d'un nouvel appareil désactivée, toute personne ayant votre mot de passe maître peut accéder à votre compte depuis n'importe quel appareil. Pour protéger votre compte sans courriel de vérification, configurez la connexion en deux étapes." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Modifications de la protection de connexion de l'appareil enregistrées" + }, "sessionsDeauthorized": { "message": "Toutes les sessions ont été révoquées" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Gérer" }, - "canManage": { - "message": "Peut gérer" + "manageCollection": { + "message": "Gérer la collection" + }, + "viewItems": { + "message": "Afficher les éléments" + }, + "viewItemsHidePass": { + "message": "Afficher les éléments, les mots de passe cachés" + }, + "editItems": { + "message": "Modifier les items" + }, + "editItemsHidePass": { + "message": "Modifier les éléments, les mots de passe cachés" }, "disable": { "message": "Désactiver" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Un problème est survenu lors de la lecture de la clé de sécurité. Veuillez réessayer." }, - "twoFactorWebAuthnWarning": { - "message": "En raison des limitations de la plate-forme, WebAuthn ne peut pas être utilisé sur toutes les applications Bitwarden. Vous devriez mettre en place un autre fournisseur d'authentification à deux facteurs afin de pouvoir accéder à votre compte lorsque WebAuthn ne peut pas être utilisé. Plateformes supportées :" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Coffre web et extensions sur un ordinateur fixe/portable avec un navigateur compatible WebAuthn (Chrome, Opera, Vivaldi ou Firefox avec FIDO U2F activé)." + "twoFactorWebAuthnWarning1": { + "message": "En raison des limitations de plate-forme, WebAuthn ne peut pas être utilisé sur toutes les applications Bitwarden. Vous devriez mettre en place un autre fournisseur d'authentification à deux facteurs afin de pouvoir accéder à votre compte lorsque WebAuthn ne peut pas être utilisé." }, "twoFactorRecoveryYourCode": { "message": "Votre code de récupération de d'authentification à deux facteurs Bitwarden" @@ -3279,7 +3378,10 @@ } }, "inviteSingleEmailDesc": { - "message": "You have 1 invite remaining." + "message": "Il vous reste 1 invitation." + }, + "inviteZeroEmailDesc": { + "message": "Il vous reste 0 invitations." }, "userUsingTwoStep": { "message": "Cet utilisateur utilise l'authentification à deux facteurs pour protéger son compte." @@ -3300,7 +3402,7 @@ "message": "Propriétaire" }, "ownerDesc": { - "message": "L’utilisateur avec l’accès le plus élevé qui peut gérer tous les aspects de votre organisation." + "message": "Gérer tous les aspects de votre organisation, y compris la facturation et les abonnements" }, "clientOwnerDesc": { "message": "Cet utilisateur doit être indépendant du fournisseur. Si le fournisseur est dissocié de l'organisation, cet utilisateur conservera la propriété de l'organisation." @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "SSO non lié." + }, "unlinkedSsoUser": { "message": "SSO dissocié pour l'utilisateur $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Approuvé" }, + "needsApproval": { + "message": "Requiert une approbation" + }, + "areYouTryingtoLogin": { + "message": "Essayez-vous de vous connecter ?" + }, + "logInAttemptBy": { + "message": "Tentative de connexion par $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Type d'appareil" + }, + "ipAddress": { + "message": "Adresse IP" + }, + "confirmLogIn": { + "message": "Confirmer la connexion" + }, + "denyLogIn": { + "message": "Refuser la connexion" + }, + "thisRequestIsNoLongerValid": { + "message": "Cette demande n'est plus valide." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Connexion confirmée pour $EMAIL$ sur $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Vous avez refusé une tentative de connexion depuis un autre appareil. Si c'était vraiment vous, essayez de vous connecter à nouveau avec l'appareil." + }, + "loginRequestHasAlreadyExpired": { + "message": "La demande de connexion a déjà expiré." + }, + "justNow": { + "message": "À l’instant" + }, + "requestedXMinutesAgo": { + "message": "Demandé il y a $MINUTES$ minutes", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Création du compte sur" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Définir les exigences pour le générateur de mot de passe." }, - "passwordGeneratorPolicyInEffect": { - "message": "Une ou plusieurs politiques de sécurité de l'organisation affectent les paramètres de votre générateur." - }, "masterPasswordPolicyInEffect": { "message": "Une ou plusieurs politiques de sécurité de l'organisation exigent que votre mot de passe principal réponde aux exigences suivantes :" }, @@ -5005,7 +5168,7 @@ "message": "Accordez et gérez l'accès d'urgence pour les contacts de confiance. Les contacts de confiance peuvent demander l'accès pour afficher ou reprendre le contrôle de votre compte en cas d'urgence. Consultez notre page d'aide pour plus d'informations et de détails sur le fonctionnement du partage à divulgation nulle de connaissance (zero knowledge sharing)." }, "emergencyAccessOwnerWarning": { - "message": "Vous êtes propriétaire d'une ou de plusieurs organisations. Si vous autorisez la prise de contrôle à un contact d'urgence, il sera en mesure d'utiliser toutes vos permissions de propriétaire après la prise de contrôle." + "message": "Vous êtes propriétaire d'une ou de plusieurs organisations. Si vous autorisez la prise de contrôle à un contact d'urgence, il sera en mesure d'utiliser toutes vos autorisations de propriétaire après la prise de contrôle." }, "trustedEmergencyContacts": { "message": "Contacts d'urgence de confiance" @@ -5232,7 +5395,7 @@ "message": "Permissions" }, "permission": { - "message": "Permission" + "message": "Autorisation" }, "accessEventLogs": { "message": "Accéder aux journaux d'événements" @@ -6514,7 +6677,7 @@ } }, "accessDenied": { - "message": "Accès Refusé. Vous n'avez pas la permission de voir cette page." + "message": "Accès Refusé. Vous n'avez pas l'autorisation d'afficher cette page." }, "masterPassword": { "message": "Mot de passe principal" @@ -6554,15 +6717,6 @@ "message": "Générateur", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Que souhaitez-vous générer ?" - }, - "passwordType": { - "message": "Type de mot de passe" - }, - "regenerateUsername": { - "message": "Régénérer le Nom d'Utilisateur" - }, "generateUsername": { "message": "Générer le Nom d'Utilisateur" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Type de Nom d'Utilisateur" - }, "plusAddressedEmail": { "message": "Courriel Adressé Plus", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Utilisez la boîte de réception du collecteur (catch-all) configurée de votre domaine." }, + "useThisEmail": { + "message": "Utiliser ce courriel" + }, "random": { "message": "Aléatoire", "description": "Generates domain-based username using random letters" @@ -6650,7 +6804,7 @@ "message": "Service" }, "unknownCipher": { - "message": "Élément inconnu, vous devrez peut-être vous connecter avec un autre compte pour accéder à cet élément." + "message": "Élément inconnu, vous devrez peut-être demander l'autorisation d'accéder à cet élément." }, "cannotSponsorSelf": { "message": "Vous ne pouvez pas réclamer pour le compte actif. Saisissez un courriel différent." @@ -6822,9 +6976,6 @@ "message": "Nom d'hôte", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Jetons d'accès API" - }, "deviceVerification": { "message": "Vérification de l'Appareil" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Aucune collection" }, - "canView": { - "message": "Peut voir" - }, - "canViewExceptPass": { - "message": "Peut voir, sauf les mots de passe" - }, - "canEdit": { - "message": "Peut modifier" - }, - "canEditExceptPass": { - "message": "Peut modifier, sauf les mots de passe" - }, "noCollectionsAdded": { "message": "Pas de collections ajoutées" }, @@ -7738,7 +7877,7 @@ "message": "Sélectionner les groupes" }, "userPermissionOverrideHelperDesc": { - "message": "Les permissions définies pour un membre remplaceront les permissions définies par le groupe de ce membre." + "message": "Les autorisations définies pour un membre remplaceront les autorisations définies par le groupe de ce membre." }, "noMembersOrGroupsAdded": { "message": "Aucun membre ou groupe ajouté" @@ -8125,7 +8264,7 @@ "message": "Exiger que les membres existants changent leurs mots de passe" }, "smProjectDeleteAccessRestricted": { - "message": "Vous n'avez pas les droits pour supprimer ce projet", + "message": "Vous n'avez pas les autorisations pour supprimer ce projet", "description": "The individual description shown to the user when the user doesn't have access to delete a project." }, "smProjectsDeleteBulkConfirmation": { @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Appareils de confiance" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Une fois authentifiés, les membres déchiffreront les données du coffre à l'aide d'une clé enregistrée sur leur appareil. La", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Les membres n'auront pas besoin d'un mot de passe maître lors de la connexion avec SSO. Le mot de passe principal est remplacé par une clé de chiffrement stockée sur le périphérique, ce qui rend ce périphérique fiable. Le premier appareil avec lequel un membre créera son compte et se connectera sera fiable. Les nouveaux appareils devront être approuvés par un périphérique de confiance existant ou par un administrateur. La", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { + "message": "politique", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" + }, + "memberDecryptionOptionTdeDescPart2": { "message": "d'organisation unique,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "politique", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "la politique", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescPart3": { "message": "SSO requise et", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "la politique", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" - }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "la politique d'administration de la restauration du compte", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "avec inscription automatique sera activée quand cette option est utilisée.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "d'administration de récupération de compte seront activées si cette option est utilisée.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Les autorisations de votre organisation ont été mises à jour, vous obligeant à définir un mot de passe principal.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limiter la suppression de collections aux propriétaires et administrateurs" }, + "limitItemDeletionDesc": { + "message": "Limite la suppression de l'élément aux membres avec l'autorisation Peut gérer" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Les propriétaires et les administrateurs peuvent gérer toutes les collections et tous les éléments" }, @@ -8544,9 +8686,6 @@ "message": "URL du serveur auto-hébergé", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Domaine de l'alias" - }, "alreadyHaveAccount": { "message": "Vous avez déjà un compte ?" }, @@ -8557,7 +8696,7 @@ "message": "Sauter directement au contenu" }, "managePermissionRequired": { - "message": "Au moins un membre ou un groupe doit avoir la permission peut gérer." + "message": "Au moins un membre ou un groupe doit avoir l'autorisation peut gérer." }, "typePasskey": { "message": "Passkey" @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Vous n'avez pas accès à la gestion de cette collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "\"Peut Gérer les Permissions\" manquant" + "grantManageCollectionWarningTitle": { + "message": "Autorisations pour Gérer la Collection manquantes" }, - "grantAddAccessCollectionWarning": { - "message": "Accorder \"Peut Gérer les Permissions\" pour permettre une gestion complète de la collection, y compris la suppression de la collection." + "grantManageCollectionWarning": { + "message": "Accorde les autorisations pour Gérer la collection pour permettre la gestion complète de la collection incluant sa suppression." }, "grantCollectionAccess": { "message": "Accorder l'accès à cette collection aux groupes ou aux membres." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "mois par membre" }, + "monthPerMemberBilledAnnually": { + "message": "mois par membre facturés annuellement" + }, "seats": { "message": "Licences" }, @@ -9322,16 +9464,16 @@ "message": "Informations sur les taxes mises à jour" }, "billingInvalidTaxIdError": { - "message": "Invalid tax ID, if you believe this is an error please contact support." + "message": "ID de taxe non valide, si vous pensez qu'il s'agit d'une erreur, veuillez contacter le support." }, "billingTaxIdTypeInferenceError": { - "message": "We were unable to validate your tax ID, if you believe this is an error please contact support." + "message": "Nous n'avons pu valider votre ID de taxes. Si vous pensez que c'est une erreur, veuillez contacter le support s'il-vous-plaît." }, "billingPreviewInvalidTaxIdError": { - "message": "Invalid tax ID, if you believe this is an error please contact support." + "message": "ID de taxe non valide, si vous pensez qu'il s'agit d'une erreur, veuillez contacter le support." }, "billingPreviewInvoiceError": { - "message": "An error occurred while previewing the invoice. Please try again later." + "message": "Une erreur s'est produite lors de la prévisualisation de la facture. Veuillez réessayer plus tard." }, "unverified": { "message": "Non vérifié" @@ -9374,7 +9516,7 @@ "message": "(Aucune collection)" }, "memberAccessReportNoCollectionPermission": { - "message": "(Aucune permission de collection)" + "message": "(Aucune Autorisation de Collection)" }, "memberAccessReportNoGroup": { "message": "(Aucun groupe)" @@ -9416,7 +9558,7 @@ "message": " Contactez le Support Client pour rétablir votre abonnement." }, "secretPeopleDescription": { - "message": "Autoriser les groupes ou les personnes à accéder à ce secret. Les permissions définies pour les personnes remplaceront les permissions définies par les groupes." + "message": "Autoriser les groupes ou les personnes à accéder à ce secret. Les autorisations définies pour les personnes remplaceront les autorisations définies par les groupes." }, "secretPeopleEmptyMessage": { "message": "Ajouter des personnes ou des groupes pour partager l'accès à ce secret" @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Algorithme de clé" }, + "sshPrivateKey": { + "message": "Clé privée" + }, + "sshPublicKey": { + "message": "Clé publique" + }, + "sshFingerprint": { + "message": "Empreinte digitale" + }, "sshKeyFingerprint": { "message": "Empreinte digitale" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Code descripteur" }, + "cannotRemoveViewOnlyCollections": { + "message": "Vous ne pouvez pas supprimer des collections avec les autorisations d'affichage uniquement : $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Avis important" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Le nom de l'organisation ne doit pas dépasser 50 caractères." }, - "resellerRenewalWarning": { - "message": "Votre abonnement sera renouvelé bientôt. Pour assurer un service sans interruption, contactez $RESELLER$ pour confirmer votre renouvellement avant $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Votre abonnement sera renouvelé bientôt. Pour assurer un service ininterrompu, contactez $RESELLER$ pour confirmer votre renouvellement avant le $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "Une facture pour votre abonnement a été émise sur $ISSUED_DATE$. Pour assurer un service sans interruption, contactez $RESELLER$ pour confirmer votre renouvellement avant $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "Une facture pour votre abonnement a été émise le $ISSUED_DATE$. Pour assurer un service ininterrompu, contactez $RESELLER$ pour confirmer votre renouvellement avant le $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "La facture de votre abonnement n'a pas été payée. Pour assurer un service sans interruption, contactez $RESELLER$ pour confirmer votre renouvellement avant $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "La facture de votre abonnement n'a pas été payée. Pour assurer un service ininterrompu, contactez $RESELLER$ pour confirmer votre renouvellement avant $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Les administrateurs ont maintenant la possibilité de supprimer les comptes de membre qui appartiennent à un domaine revendiqué." + }, + "deleteManagedUserWarningDesc": { + "message": "Cette action supprimera le compte du membre, incluant tous les éléments de son coffre. Cela remplace l'action précédente de Supprimer." + }, + "deleteManagedUserWarning": { + "message": "Supprimer est une nouvelle action !" + }, + "seatsRemaining": { + "message": "Vous avez $REMAINING$ places restantes sur $TOTAL$ attribuées à cette organisation. Contactez votre fournisseur pour gérer votre abonnement.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Organisation existante" + }, + "selectOrganizationProviderPortal": { + "message": "Sélectionnez une organisation à ajouter à votre Portail fournisseur." + }, + "noOrganizations": { + "message": "Il n'y a aucune organisation à lister" + }, + "yourProviderSubscriptionCredit": { + "message": "Votre abonnement à un fournisseur recevra un crédit pour tout temps restant dans l'abonnement de l'organisation." + }, + "doYouWantToAddThisOrg": { + "message": "Voulez-vous ajouter cette organisation à $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Organisation existante ajoutée" + }, + "assignedExceedsAvailable": { + "message": "Les places assignées dépassent les places disponibles." } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 3e2c1161067b..f5cd301fc5f3 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Que tipo de elemento é este?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Nome do elemento" }, - "cannotRemoveViewOnlyCollections": { - "message": "Non podes eliminar coleccións con permisos de só lectura: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "Non podes eliminar coleccións con permisos de só lectura: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 6db8255c46ea..27ea87bcfe40 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "מאיזה סוג פריט זה?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "שם בסיס הדומיין", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "לדוגמא", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "שלח" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "גרסה $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "צור סיסמה חדשה" - }, "length": { "message": "אורך" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "אזור מסוכן" }, - "dangerZoneDesc": { - "message": "זהירות, פעולות אלה לא ניתנות לביטול!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "בטל הרשאות סשנים" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "בכדי להמשיך הסשן הנוכחי ינותק, ותדרש להזין את פרטי הכניסה החדשים וגם את פרטי האימות הדו-שלבי, אם הוא מאופשר. כל הסשנים הפעילים במכשירים אחרים ישארו פעילים עד שעה ממועד הכניסה החדשה." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "הוסרה ההרשאה מכל הסשנים" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "נהל" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "בטל" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "היתה בעיה בקריאת מפתח האבטחה. נסה בשנית." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "קוד השחזור שלך עבור כניסה דו שלבית לBitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "משתמש זה הפעיל כניסה דו שלבית כדי להגן על חשבונו." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "הגדר דרישות מינימום במחולל הסיסמאות." }, - "passwordGeneratorPolicyInEffect": { - "message": "מדיניות ארגונית אחת או יותר משפיעה על הגדרות המחולל שלך." - }, "masterPasswordPolicyInEffect": { "message": "אחד או יותר מכללי מדיניות הארגון דורשים שסיסמתך הראשית תעמוד בדרישות הבאות:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 25f7bc6e6f7b..266a868e435a 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "यह किस प्रकार का आइटम है?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 610b63d68c12..d5675a9cca32 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Rizični korisnici" }, + "atRiskMembersWithCount": { + "message": "Izloženi članovi ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Izložene aplikacije ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Ovi članovi se prijavljuju u aplikacije slabim, izloženim ili ponovno korištenim lozinkama." + }, + "atRiskApplicationsDescription": { + "message": "Ove aplikacije imaju slabe, izložene ili ponovno korištene lozinke." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Ovi članovi se u $APPNAME$ prijavljuju slabim, izloženim ili ponovno korištenim lozinkama.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Ukupno korisnika" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Ukupno aplikacija" }, + "unmarkAsCriticalApp": { + "message": "Odznači kao kritičnu aplikaciju" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritična aplikacija uspješno odznačena" + }, "whatTypeOfItem": { "message": "Koja je ovo vrsta stavke?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Sigurno želiš trajno izbrisati ovu mapu?" + }, "baseDomain": { "message": "Primarna domena", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Naziv stavke" }, - "cannotRemoveViewOnlyCollections": { - "message": "S dopuštenjima samo za prikaz ne možeš ukloniti zbirke: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "npr.", "description": "Short abbreviation for 'example'." @@ -1128,15 +1170,24 @@ "verifyIdentity": { "message": "Potvrdi svoj identitet" }, + "weDontRecognizeThisDevice": { + "message": "Ne prepoznajemo ovaj uređaj. Za potvrdu identiteta unesi kôd poslan e-poštom." + }, + "continueLoggingIn": { + "message": "Nastavi prijavu" + }, "whatIsADevice": { - "message": "What is a device?" + "message": "Što je uređaj?" }, "aDeviceIs": { - "message": "A device is a unique installation of the Bitwarden app where you have logged in. Reinstalling, clearing app data, or clearing your cookies could result in a device appearing multiple times." + "message": "Uređaj je jedinstvena instalacija Bitwarden aplikacije gdje si prijavljen/a. Ponovna instalacija, brisanje podataka aplikacije ili kolačića može rezultirati višestrukim prikazom uređaja." }, "logInInitiated": { "message": "Pokrenuta prijava" }, + "logInRequestSent": { + "message": "Zahtjev poslan" + }, "submit": { "message": "Pošalji" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Obavijest je poslana na tvoj uređaj." }, + "notificationSentDevicePart1": { + "message": "Otključaj Bitwarden na svojem uređaju ili na " + }, + "areYouTryingToAccessYourAccount": { + "message": "Pokušavaš li pristupiti svom računu?" + }, + "accessAttemptBy": { + "message": "$EMAIL$ pokušava pristupiti", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Potvrdi pristup" + }, + "denyAccess": { + "message": "Odbij pristup" + }, + "notificationSentDeviceAnchor": { + "message": "web trezoru" + }, + "notificationSentDevicePart2": { + "message": "Provjeri slaže li se jedinstvena fraza s ovdje prikazanom prije odobravanja." + }, + "notificationSentDeviceComplete": { + "message": "Otključaj Bitwarden na svojem uređaju. Provjeri slaže li se jedinstvena fraza s ovdje prikazanom prije odobravanja." + }, "aNotificationWasSentToYourDevice": { "message": "Obavijest je poslana na tvoj uređaj" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Provjeri je li trezor otključan i slaže li se jedinstvena fraza s drugim uređajem" - }, "versionNumber": { "message": "Verzija $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Izbjegavaj dvosmislene znakove", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Ponovno generiraj lozinku" - }, "length": { "message": "Duljina" }, @@ -1722,10 +1797,10 @@ "message": "Molimo, ponovno se prijavi." }, "currentSession": { - "message": "Current session" + "message": "Trenutna sesija" }, "requestPending": { - "message": "Request pending" + "message": "Zahtjev u tijeku" }, "logBackInOthersToo": { "message": "Molimo, ponovno se prijavi. Ako koristiš druge aplikacije Bitwarden i u njima napravi odjavu/prijavu." @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "OPASNA zona!" }, - "dangerZoneDesc": { - "message": "Pažljivo, ove akcije su konačne i ne mogu se poništiti!" - }, - "dangerZoneDescSingular": { - "message": "Pažljivo, ova radnja se ne može poništiti!" - }, "deauthorizeSessions": { "message": "Deautoriziraj sesije" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Ako nastaviš, trenutna sesija će biti zatvorena, što će zahtijevati ponovnu prijavu uklljučujući i prijavu dvostrukom autentifikacijom, ako je ona aktivna. Aktivne sesije na drugim uređajima mogu ostati aktivne još jedan sat." }, + "newDeviceLoginProtection": { + "message": "Prijava novog uređaja" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Isključi zaštitu prijave novih uređaja" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Uključi zaštitu prijave novih uređaja" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Za prestanak dobivanja e-pošte kada se prijaviš s novog uređaja, nastavi niže." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Za dobivanje e-pošte kada se prijaviš s novog uređaja, nastavi niže." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Ako je zaštita prijave novih uređaja isključena, bilo tko s tvojom glavnom lozinkom može pristupiti tvom računu s bilo kojeg uređaja. Za zaštitu svog računa bez potvrde e-poštom, uključi dvostruku autentifikaciju." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Promjene zaštite prijave novih uređaja su spremljene" + }, "sessionsDeauthorized": { "message": "Sve sesije deautorizirane" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Upravljaj" }, - "canManage": { - "message": "Može upravljati" + "manageCollection": { + "message": "Upravljaj zbirkom" + }, + "viewItems": { + "message": "Prikaz stavke" + }, + "viewItemsHidePass": { + "message": "Prikaz stavke, skrivene lozinke" + }, + "editItems": { + "message": "Uredi stavke" + }, + "editItemsHidePass": { + "message": "Uredi stavke, skrivene lozinke" }, "disable": { "message": "Onemogući" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Došlo je do pogreške kod očitavanja sigurnosnog ključa. Pokušaj ponovno." }, - "twoFactorWebAuthnWarning": { - "message": "Zbog platformskih ograničenja, WebAuthn nije moguće koristiti s Bitwarden aplikacijama na svim platformama. Za pristup računu kada nije moguće koristiti WebAuthn, trebalo bi uključiti drugog pružatelja prijave dvostrukom autentifikacijom. Platforme na kojima je WebAuthn podržan:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web trezor i proširenja preglednika na stolnim/prijenosnim računalima s WebAuthn podržanim preglednikom (npr. Chrome, Opera, Vivaldi ili Firefox s omogućenim WebAuthn)." + "twoFactorWebAuthnWarning1": { + "message": "Zbog platformskih ograničenja, WebAuthn nije moguće koristiti sa svim Bitwarden aplikacijama. Uključi drugi način dvostruke autentifikacije za pristup računu kada se ne može koristiti WebAuthn." }, "twoFactorRecoveryYourCode": { "message": "Tvoj kôd za oporavak Bitwarden prijave dvostrukom autentifikacijom" @@ -3279,7 +3378,10 @@ } }, "inviteSingleEmailDesc": { - "message": "You have 1 invite remaining." + "message": "Imaš jednu preostalu pozivnicu." + }, + "inviteZeroEmailDesc": { + "message": "Nemaš preostalih pozivnica." }, "userUsingTwoStep": { "message": "Ovaj korisnik upotrebljava prijavu u dva koraka za zaštitu svog računa." @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "SSO odspojen." + }, "unlinkedSsoUser": { "message": "Odspojen SSO za korisnika $ID$.", "placeholders": { @@ -3781,13 +3886,74 @@ "message": "Uređaj" }, "loginStatus": { - "message": "Login status" + "message": "Status prijave" }, "firstLogin": { - "message": "First login" + "message": "Prva prijava" }, "trusted": { - "message": "Trusted" + "message": "Pouzdan" + }, + "needsApproval": { + "message": "Zahtjeva odobrenje" + }, + "areYouTryingtoLogin": { + "message": "Pokušavaš li se prijaviti?" + }, + "logInAttemptBy": { + "message": "$EMAIL$ se pokušava prijaviti", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Vrsta uređaja" + }, + "ipAddress": { + "message": "IP Adresa" + }, + "confirmLogIn": { + "message": "Potvrdi prijavu" + }, + "denyLogIn": { + "message": "Odbij prijavu" + }, + "thisRequestIsNoLongerValid": { + "message": "Ovaj zahtjev više nije valjan." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Prijava za $EMAIL$ potvrđena na uređaju $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Odbijena je prijava na drugom uređaju. Ako si ovo stvarno ti, pokušaj se ponovno prijaviti uređajem." + }, + "loginRequestHasAlreadyExpired": { + "message": "Zahtjev za prijavu je već istekao." + }, + "justNow": { + "message": "Upravo" + }, + "requestedXMinutesAgo": { + "message": "Zatraženo prije $MINUTES$ minute/a", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } }, "creatingAccountOn": { "message": "Stvaranje računa na" @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Postavi pravila sigurnosti koje mora zadovoljiti generator lozinki." }, - "passwordGeneratorPolicyInEffect": { - "message": "Jedna ili više organizacijskih pravila utječe na postavke generatora." - }, "masterPasswordPolicyInEffect": { "message": "Jedna ili više organizacijskih pravila zahtijeva da tvoja glavna lozinka ispunjava sljedeće uvjete:" }, @@ -5680,17 +5843,17 @@ "message": "Greška" }, "decryptionError": { - "message": "Decryption error" + "message": "Pogreška pri dešifriranju" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden nije mogao dešifrirati sljedeće stavke trezora." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Kontaktiraj službu za korisnike", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "kako bi izbjegli gubitak podataka.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Što želiš generirati?" - }, - "passwordType": { - "message": "Tip lozinke" - }, - "regenerateUsername": { - "message": "Ponovno generiraj korisničko ime" - }, "generateUsername": { "message": "Generiraj korisničko ime" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Tip korisničkog imena" - }, "plusAddressedEmail": { "message": "Plus adresa e-pošte", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Koristi konfigurirani catch-all sandučić svoje domene." }, + "useThisEmail": { + "message": "Koristi ovu e-poštu" + }, "random": { "message": "Nasumično", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Naziv poslužitelja", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token za API pristup" - }, "deviceVerification": { "message": "Provjera uređaja" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Nema zbirke" }, - "canView": { - "message": "Može vidjeti" - }, - "canViewExceptPass": { - "message": "Može vidjeti, osim lozinke" - }, - "canEdit": { - "message": "Može urediti" - }, - "canEditExceptPass": { - "message": "Može urediti, osim lozinke" - }, "noCollectionsAdded": { "message": "Niti jedna zbirka nije dodana" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Pouzdani uređaji" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Jednom autentificirani, korisnici će moći dešifrirati podatke u trezoru koristeći ključ spremljen na njihovom uređaju. ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Članovi neće trebati glavnu lozinku kada se prijavljuju putem SSO-a. Glavnu lozinku zamjenjuje ključem za šifriranje pohranjen na uređaju, što taj uređaj čini pouzdanim. Prvi uređaj na kojem član kreira svoj račun i s kojeg se prijavi bit će pouzdan. Nove uređaje morat će odobriti ili postojeći pouzdani uređaj ili administrator. Pravilo", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "pravilo Isključive organizacije", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "isključive organizacije,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": ",", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "pravilo obavezne SSO autentifikacije", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "obavezne SSO prijave", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "pravilo i ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "i", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "pravilo administracije povrata računa", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "administracije oporavka računa", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "s automatskim učlanjenjem će se uključiti kada se koristi ova opcija.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "biti će uključeni ako se koristi ova opcija.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Moraš postaviti glavnu lozinku jer su dopuštenja tvoje organizacije ažurirana.", @@ -8275,16 +8414,16 @@ "message": "Odobri zahtjev" }, "deviceApproved": { - "message": "Device approved" + "message": "Uređaj odobren" }, "deviceRemoved": { - "message": "Device removed" + "message": "Uređaj uklonjen" }, "removeDevice": { - "message": "Remove device" + "message": "Ukloni uređaj" }, "removeDeviceConfirmation": { - "message": "Are you sure you want to remove this device?" + "message": "Sigurno želiš ukoniti ovaj uređaj?" }, "noDeviceRequests": { "message": "Nema zahtjeva na čekanju" @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Omogući brisanje zbirki samo vlasnicima i adminima" }, + "limitItemDeletionDesc": { + "message": "Ograniči brisanje stavke samo članovima koji imaju dozvolu „Moguće upravljanje”" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Vlasnici i admini mogu upravljati svim zbirkama i stavkama" }, @@ -8544,9 +8686,6 @@ "message": "URL vlastitog poslužitelja", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domene" - }, "alreadyHaveAccount": { "message": "Već imaš račun?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Nemaš pristup za upravljanje ovom zbirkom." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Neodstaju prava „Moguće upravljanje”" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Dodijeli prava „Moguće upravljanje” kako bi dozvolili puni pristup zbirkama uključujući brisanje." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Dodijeli grupama i članovima pristup ovoj zbirki." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "mjesečno po korisniku" }, + "monthPerMemberBilledAnnually": { + "message": "mjesečno po članu, naplata godišnje" + }, "seats": { "message": "Mjesta" }, @@ -9322,16 +9464,16 @@ "message": "Ažurirane porezne informacije" }, "billingInvalidTaxIdError": { - "message": "Invalid tax ID, if you believe this is an error please contact support." + "message": "Neispravni porezni broj. Ako misliš da je broj ispravan, kontaktiraj podršku." }, "billingTaxIdTypeInferenceError": { - "message": "We were unable to validate your tax ID, if you believe this is an error please contact support." + "message": "Nismo mogli provjeriti tvoj porezni broj. Ako misliš da je broj ispravan, kontaktiraj podršku." }, "billingPreviewInvalidTaxIdError": { - "message": "Invalid tax ID, if you believe this is an error please contact support." + "message": "Neispravni porezni broj. Ako misliš da je broj ispravan, kontaktiraj podršku." }, "billingPreviewInvoiceError": { - "message": "An error occurred while previewing the invoice. Please try again later." + "message": "Greška kod pregleda fakture. Pokušaj ponovno kasnije." }, "unverified": { "message": "Nepotvrđeno" @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Algoritam ključa" }, + "sshPrivateKey": { + "message": "Privatni ključ" + }, + "sshPublicKey": { + "message": "Javni ključ" + }, + "sshFingerprint": { + "message": "Otisak prsta" + }, "sshKeyFingerprint": { "message": "Otisak prsta" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Šifra uplate" }, + "cannotRemoveViewOnlyCollections": { + "message": "S dopuštenjima samo za prikaz ne možeš ukloniti zbirke: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Važna napomena" }, @@ -9986,13 +10146,13 @@ "message": "Ukloni članove" }, "devices": { - "message": "Devices" + "message": "Uređaji" }, "deviceListDescription": { - "message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now." + "message": "Tvoj račun je prijavljen na svakom od ovih uređaja. Ako ne prepoznaješ uređaj, odmah ga ukloni." }, "deviceListDescriptionTemp": { - "message": "Your account was logged in to each of the devices below." + "message": "Tvoj račun je prijavljen na svakom od ovih uređaja." }, "claimedDomains": { "message": "Potvrđene domene" @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Naziv organizacije ne može biti duži od 50 znakova." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Tvoja će se pretplata uskoro obnoviti. Za neprekinutu uslugu, kontaktiraj $RESELLER$ za potvrdu obnove prije $RENEWAL_DATE$.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Za tvoju je pretplatu $ISSUED_DATE$ izdana faktura. Za neprekinutu uslugu, kontaktiraj $RESELLER$ za potvrdu obnove prije $DUE_DATE$.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Faktura za tvoju pretplatu nije plaćena. Za neprekinutu uslugu, kontaktiraj $RESELLER$ za potvrdu obnovu prije $GRACE_PERIOD_END$.", "placeholders": { "reseller": { @@ -10123,18 +10283,67 @@ } }, "restartOrganizationSubscription": { - "message": "Organization subscription restarted" + "message": "Organizacijska pretplata ponovno pokrenuta" }, "restartSubscription": { - "message": "Restart your subscription" + "message": "Ponovno pokreni pretplatu" }, "suspendedManagedOrgMessage": { - "message": "Contact $PROVIDER$ for assistance.", + "message": "Kontaktiraj $PROVIDER$ za pomoć.", "placeholders": { "provider": { "content": "$1", "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "Imaš još $REMAINING$ preostalih sjedišta od $TOTAL$ dodijeljenih ovog organizaciji. Kontaktiraj svog pružatelja usluge za upravljanje pretplatom.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Postojeća organizacija" + }, + "selectOrganizationProviderPortal": { + "message": "Odaberi organizaciju koju želiš dodati na svoj portal pružatelja usluga." + }, + "noOrganizations": { + "message": "Nema organizaciji za prikaz" + }, + "yourProviderSubscriptionCredit": { + "message": "Tvoja pretplata davatelja usluga dobit će kredit za preostalo vrijeme u organizacijskoj pretplati." + }, + "doYouWantToAddThisOrg": { + "message": "IP Adresa", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Postojeća organizacija dodana" + }, + "assignedExceedsAvailable": { + "message": "Dodijeljene licence premašuju dostupne licence." } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 163355b20d94..bdc55b96d3cb 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Veszélyes tagok" }, + "atRiskMembersWithCount": { + "message": "Veszélyes tagok ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Kockázatos alkalmazások ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Ezek a tagok gyenge, nyílt vagy újrafelhasznált jelszavakkal jelentkeznek be az alkalmazásokba." + }, + "atRiskApplicationsDescription": { + "message": "Ezeknél az alkalmazásoknál gyenge, nyílt vagy újra felhasznált jelszavak vannak." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Ezek a tagok gyenge, nyilvános vagy újrahasznált jelszavakkal jelentkeznek be $APPNAME$ alkalmazásba.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Összes tagok száma" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Összes alkalmazás" }, + "unmarkAsCriticalApp": { + "message": "Ktritkus alkalmazás jelölés eltávolítása" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "A ktritkus alkalmazás jelölés eltávolítása sikeres volt." + }, "whatTypeOfItem": { "message": "Milyen típusú elem ez?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Biztosan véglegesen törlésre kerüljön ez a mappa?" + }, "baseDomain": { "message": "Alap domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Elem neve" }, - "cannotRemoveViewOnlyCollections": { - "message": "Nem távolíthatók el a csak megtekintési engedéllyel bíró gyűjtemények: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "Példa:", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Személyazonosság ellenőrzése" }, + "weDontRecognizeThisDevice": { + "message": "Nem ismerhető fel ez az eszköz. Írjuk be az email címünkre küldött kódot a személyazonosság igazolásához." + }, + "continueLoggingIn": { + "message": "A bejelentkezés folytatása" + }, "whatIsADevice": { "message": "Mi az eszköz?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "A bejelentkezés elindításra került." }, + "logInRequestSent": { + "message": "A kérés elküldésre került." + }, "submit": { "message": "Elküldés" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A rendszer értesítést küldött az eszközre." }, + "notificationSentDevicePart1": { + "message": "A Bitwarden zárolás feloldása az eszközön vagy: " + }, + "areYouTryingToAccessYourAccount": { + "message": "A fiókhoz próbálunk hozzáférni?" + }, + "accessAttemptBy": { + "message": "Bejelentkezési kísérlet $EMAIL$ segítségével", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Hozzáférés megerősítése" + }, + "denyAccess": { + "message": "Hozzáférés megtagadása" + }, + "notificationSentDeviceAnchor": { + "message": "webalkalmazás" + }, + "notificationSentDevicePart2": { + "message": "Jóváhagyás előtt győződjünk meg arról, hogy az ujjlenyomat kifejezés megegyezik az alábbi kifejezéssel." + }, + "notificationSentDeviceComplete": { + "message": "Oldjuk fel a Bitwarden zárolását az eszközön. Jóváhagyás előtt győződjünk meg arról, hogy az ujjlenyomat kifejezés megegyezik az alábbi kifejezéssel." + }, "aNotificationWasSentToYourDevice": { "message": "Egy értesítés lett elküldve az eszközre." }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Ellenőrizzük, hogy a széf feloldásra került és az ujjlenyomat kifejezés egyezik a másik eszközön levővel." - }, "versionNumber": { "message": "Verzió: $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Félreérthető karakterek mellőzése", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Jelszó újragenerálása" - }, "length": { "message": "Hossz" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Veszélyes terület" }, - "dangerZoneDesc": { - "message": "Óvatosan! Ezeket a műveleteket nem lehet visszaállítani." - }, - "dangerZoneDescSingular": { - "message": "Óvatosan! Ezeket a műveleteket nem lehet visszaállítani." - }, "deauthorizeSessions": { "message": "Munkamenetek hitelesítésének eldobása" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "A folytatásban s felhasználó kiléptetésre kerül az aktuális munkamenetből, szükséges az ismételt bejelentkezés. Ismételten megjelenik a kétlépcsős bejelentkezés, ha az engedélyezett. Más eszközök aktív munkamenetei akár egy óráig is aktívak maradhatnak." }, + "newDeviceLoginProtection": { + "message": "Új eszköz bejelentkezés" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Az új eszköz bejelentkezési védelmének kikapcsolása" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Az új eszköz bejelentkezési védelmének bekapcsolása" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Folytassuk az alábbiakkal a Bitwarden által küldendő ellenőrző emailek kikapcsolásához, amikor új eszközről jelentkezünk be." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Folytassuk az alábbiakkal a Bitwarden által küldendő ellenőrző emailek bekapcsolásához, amikor új eszközről jelentkezünk be." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Ha az új eszköz bejelentkezési védelme ki van kapcsolva, a mesterjelszó birtokában bárki hozzáférhet fiókhoz bármilyen eszközről. Ha meg szeretnénk védeni fiókot ellenőrző emailek nélkül, állítsunk be kétlépcsős bejelentkezést." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Az új eszköz bejelentkezés védelmi módosítások mentésre kerültek." + }, "sessionsDeauthorized": { "message": "Az összes munkamenet hitelesítése eldobásra került." }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Kezelés" }, - "canManage": { - "message": "Kezelhet" + "manageCollection": { + "message": "Gyűjtemény kezelése" + }, + "viewItems": { + "message": "Elemek megtekintése" + }, + "viewItemsHidePass": { + "message": "Elemek megtekintése, rejtett jelszavak" + }, + "editItems": { + "message": "Elemek szerkesztése" + }, + "editItemsHidePass": { + "message": "Elemek szerkesztése, rejtett jelszavak" }, "disable": { "message": "Letiltás" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Probléma lépett fel a biztonsági kulcs olvasásakor. Próbáljuk újra." }, - "twoFactorWebAuthnWarning": { - "message": "A platform korlátozások miatt nem lehetséges minden Bitwarden alkalmazásban YubiKey eszközt használni. Engedélyezzünk egy másik kétlépcsős bejelentkezés szolgáltatót, hogy hozzáférhessünk a fiókhoz akkor is, ha a YubiKey nem használható. Támogatott platformok:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web széf és böngésző kiegészítők asztali gépen / laptopon engedélyezett U2F böngészővel (Chrome, Opera, Vivaldi, vagy Firefox bekapcsolt FIDO U2F-fel)." + "twoFactorWebAuthnWarning1": { + "message": "A platform korlátozások miatt nem lehetséges minden Bitwarden alkalmazásban a WebAuthn használata. Be kell üzemelni egy másik kétlépcsős bejelentkezés szolgáltatót, hogy hozzáférhessünk a fiókhoz akkor is, ha a WebAuthn nem használható." }, "twoFactorRecoveryYourCode": { "message": "Bitwarden kétlépcsős bejelentkezés helyreállító kód" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "1 meghívó maradt." }, + "inviteZeroEmailDesc": { + "message": "0 meghívó maradt." + }, "userUsingTwoStep": { "message": "Ez a felhasználó kétlépcsős bejelentkezést használ fiókja védelmére." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Az SSO szétkapcsolásra került." + }, "unlinkedSsoUser": { "message": "SSO szétkapcsolva $ID$ felhasználónál.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Megbízható" }, + "needsApproval": { + "message": "Jóváhagyást igényel" + }, + "areYouTryingtoLogin": { + "message": "Megpróbálunk bejelentkezni?" + }, + "logInAttemptBy": { + "message": "Bejelentkezési kísérlet $EMAIL$ segítségével", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Eszköz típus" + }, + "ipAddress": { + "message": "IP cím" + }, + "confirmLogIn": { + "message": "Bejelentkezés megerősítése" + }, + "denyLogIn": { + "message": "Bejelentkezés megtagadása" + }, + "thisRequestIsNoLongerValid": { + "message": "A kérés a továbbiakban már nem érvényes." + }, + "logInConfirmedForEmailOnDevice": { + "message": "A bejelelentketés $EMAIL$ email címmel megerősítésre került $DEVICE$ eszközön.", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Megtagadásra került egy bejelentkezési kísérletet egy másik eszközről. Ha valóban mi voltunk, próbáljunk meg újra bejelentkezni az eszközzel." + }, + "loginRequestHasAlreadyExpired": { + "message": "A bejelentkezési kérés már lejárt." + }, + "justNow": { + "message": "Éppen most" + }, + "requestedXMinutesAgo": { + "message": "Kérve $MINUTES$ perccel ezelőtt", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Fiók létrehozása:" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "A minimális követelmények beállítása a jelszó generáló konfigurációhoz." }, - "passwordGeneratorPolicyInEffect": { - "message": "Egy vagy több szervezeti szabály érinti a generátor beállításokat." - }, "masterPasswordPolicyInEffect": { "message": "Egy vagy több szervezeti szabály előírja a mesterjelszóhoz a következő követelményeket:" }, @@ -6554,15 +6717,6 @@ "message": "Generátor", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Mit szeretnénk generálni?" - }, - "passwordType": { - "message": "Jelszótípus" - }, - "regenerateUsername": { - "message": "Felhasználónév ismételt generálása" - }, "generateUsername": { "message": "Felhasználónév generálása" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Felhasználónév típusa" - }, "plusAddressedEmail": { "message": "További címzési email cím", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Használjuk a tartomány konfigurált összes befogási bejövő postaládát." }, + "useThisEmail": { + "message": "Ezen email használata" + }, "random": { "message": "Véletlen", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Kiszolglónév", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API hozzáférési vezérjel" - }, "deviceVerification": { "message": "Eszköz ellenőrzés" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Nincs gyűjtemény." }, - "canView": { - "message": "Megtekintheti" - }, - "canViewExceptPass": { - "message": "Megtekintheti, kivéve a jelszavakat" - }, - "canEdit": { - "message": "Szerkesztheti" - }, - "canEditExceptPass": { - "message": "Szerkesztheti, kivéve a jelszavakat" - }, "noCollectionsAdded": { "message": "Nem lett gyűjtemény hozzáadva." }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Megbízható eszközök" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "A hitelesítés után a tagok az eszközükön tárolt kulccsal visszafejtik a tároló adatait. Ehhez", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "A tagoknak nincs szükségük mesterjelszóra az egyszeri bejelentkezéskor. A mesterjelszót az eszközön tárolt titkosítási kulccsal helyettesítjük, így az eszköz megbízható. Az az első eszköz, amelyen a tag létrehozza a fiókját és bejelentkezik, megbízható lesz. Az új eszközöket egy meglévő megbízható eszköznek vagy egy rendszergazdának kell jóváhagynia. Az", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "önálló szervezet", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "rendszabály,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO szükséges", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "az SSO szükséges", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "rendszabály és ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "rendszabály és", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "fiók helyreállítási adminisztráció", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "a fiók helyreállítási adminisztráció", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "automatikus regisztrációval rendelkező rendszabály kerül bekapcsolásra, ha ezt az opció van használatban.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "rendszabály bekapcsolásra kerül, ha ezt a lehetőséget használjuk.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "A szervezeti engedélyek frissítésre kerültek, ezért be kell állítani egy mesterjelszót.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "A gyűjtemény törlésének korlátozása tulajdonosokra és adminisztrátorokra" }, + "limitItemDeletionDesc": { + "message": "Az elemek törlésének korlátozása a Kezelheti jogosultsággal rendelkező tagokra" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Saját üzemeltetésű szerver webcím", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Áldomain" - }, "alreadyHaveAccount": { "message": "Van már saját fiók?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Nincs jogosultság ennek a gyűjteménynek a kezelésére." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Hiányzó gyűjtemény kezelési engedélyek" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Adjunk gyűjtemény kezelési engedélyeket, hogy lehetővé tegyük a teljes gyűjtemény kezelést, beleértve a gyűjtemény törlését is." }, "grantCollectionAccess": { "message": "Adjunk hozzáférést csoportoknak vagy személyeknek eennél a gyűjteménynél." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "hónap tagonként éves számlázással" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Kulcs algoritmus" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Ujjlenyomat" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Leíró kód" }, + "cannotRemoveViewOnlyCollections": { + "message": "Nem távolíthatók el a csak megtekintési engedéllyel bíró gyűjtemények: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Fontos megjegyzés" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "A szervezet neve nem haladhatja meg az 50 karaktert." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Az előfizetés hamarosan megújul. A folyamatos szolgáltatás biztosítása érdekében lépjünk kapcsolatba $RESELLER$ viszonteladóval és erősítsük meg a megújítást $RENEWAL_DATE$ előtt.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Az előfizetésről szóló számla kiállítása $ISSUED_DATE$ napon történt. A megszakítás nélküli szolgáltatás biztosítása érdekében lépjünk kapcsolatba $RESELLER$ viszonteladóval és erősítsük meg a megújítást $DUE_DATE$ előtt.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Az előfizetésről szóló számla nem lett kfizetve. A folyamatos szolgáltatás biztosítása érdekében lépjünk kapcsolatba $RESELLER$ viszonteladóval és erősítsük meg a megújítást $GRACE_PERIOD_END$ előtt.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Az adminisztrátorok mostantól törölhetik az igényelt tartományhoz tartozó tagi fiókokat." + }, + "deleteManagedUserWarningDesc": { + "message": "Ez a művelet törli a tagi fiókot, beleértve a tárolókban lévő összes elemet. Ez felváltja az előző Eltávolítás műveletet." + }, + "deleteManagedUserWarning": { + "message": "A törlés új művelet!" + }, + "seatsRemaining": { + "message": "$REMAINING$ hely maradt a szervezethez rendelt $TOTAL$ helyből. Az előfizetés kezeléséhez forduljunk a szolgáltatóhoz.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Létező szervezet" + }, + "selectOrganizationProviderPortal": { + "message": "Válasszuk ki a szolgáltatói portálhoz hozzáadni kívánt szervezetet." + }, + "noOrganizations": { + "message": "Nincsenek listázható szervezetek." + }, + "yourProviderSubscriptionCredit": { + "message": "A szolgáltatói előfizetés jóváírást kap a szervezet előfizetésből hátralévő időre." + }, + "doYouWantToAddThisOrg": { + "message": "Hozzáadásra kerüljön ez a szervezet $PROVIDER$ szolgáltatáshoz?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "A létező szervezet hozzáadásra került." + }, + "assignedExceedsAvailable": { + "message": "A hozzárendelt helyek száma meghaladja a rendelkezésre álló helyek számát." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 3b861fb16da9..565cb743eab8 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Jenis barang apa ini?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Domain dasar", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "cth.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Kirim" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Versi $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Buat Ulang Sandi" - }, "length": { "message": "Panjang" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Zona Bahaya" }, - "dangerZoneDesc": { - "message": "Hati-hati, tindakan ini tidak bisa dibatalkan!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Batalkan Otorisasi Sesi" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Melanjutkan juga akan mengeluarkan Anda dari sesi saat ini, mengharuskan Anda untuk masuk kembali. Anda juga akan diminta untuk masuk dua langkah lagi, jika diaktifkan. Sesi aktif di perangkat lain dapat terus aktif hingga satu jam." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Semua Sesi Dicabut Izinnya" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Kelola" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Nonaktifkan" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Ada masalah saat membaca kunci keamanan. Coba lagi." }, - "twoFactorWebAuthnWarning": { - "message": "Karena keterbatasan platform, WebAuthn tidak dapat digunakan di semua aplikasi Bitwarden. Anda harus mengaktifkan penyedia proses masuk dua langkah lainnya sehingga Anda dapat mengakses akun Anda saat WebAUthn tidak dapat digunakan. Platform yang didukung:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Brankas web dan ekstensi browser di desktop / laptop dengan browser yang mendukung WebAuthn (Chrome, Opera, Vivaldi, atau Firefox dengan FIDO U2F diaktifkan)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Kode pemulihan masuk dua langkah Bitwarden Anda" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Pengguna ini menggunakan proses masuk dua langkah untuk melindungi akun mereka." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "SSO tidak ditautkan untuk pengguna $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Tetapkan persyaratan minimum untuk konfigurasi pembuat kata sandi." }, - "passwordGeneratorPolicyInEffect": { - "message": "Satu atau beberapa kebijakan organisasi memengaruhi pengaturan generator Anda." - }, "masterPasswordPolicyInEffect": { "message": "Satu atau lebih kebijakan organisasi memerlukan kata sandi utama Anda untuk memenuhi persyaratan berikut:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Jenis kata sandi" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Jenis nama pengguna" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Acak", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Verifikasi Perangkat" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 8ce01a4737db..d05dec8e603d 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "Membri a rischio ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Applicazioni a rischio ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Questi membri accedono ad applicazioni con parole d'accesso deboli, esposte, o riutilizzate." + }, + "atRiskApplicationsDescription": { + "message": "Queste applicazioni hanno parole d'accesso deboli, esposte o riutilizzate." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Questi membri stanno entrando in $APPNAME$ con parole d'accesso deboli, esposte, o riutilizzate.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Che tipo di elemento è questo?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Sei sicuro di voler eliminare definitivamente questo cartella?" + }, "baseDomain": { "message": "Dominio di base", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Nome elemento" }, - "cannotRemoveViewOnlyCollections": { - "message": "Non puoi rimuovere raccolte con i soli permessi di visualizzazione: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "es.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verifica la tua identità" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "Cos'è un dispositivo?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Login avviato" }, + "logInRequestSent": { + "message": "Richiesta inviata" + }, "submit": { "message": "Invia" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Una notifica è stata inviata al tuo dispositivo." }, + "notificationSentDevicePart1": { + "message": "Sblocca Bitwarden sul tuo dispositivo o su " + }, + "areYouTryingToAccessYourAccount": { + "message": "Stai cercando di accedere al tuo account?" + }, + "accessAttemptBy": { + "message": "Tentativo di accesso di $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Conferma accesso" + }, + "denyAccess": { + "message": "Nega accesso" + }, + "notificationSentDeviceAnchor": { + "message": "app web" + }, + "notificationSentDevicePart2": { + "message": "Assicurarsi che la frase di impronta digitale corrisponda a quella sottostante prima dell'approvazione." + }, + "notificationSentDeviceComplete": { + "message": "Sblocca Bitwarden sul tuo dispositivo. Assicurati che la frase di impronta digitale corrisponda a quella sottostante prima di approvare." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Versione $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Rigenera password" - }, "length": { "message": "Lunghezza" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Zona pericolosa" }, - "dangerZoneDesc": { - "message": "Attento, queste azioni non sono reversibili!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Annulla autorizzazione sessioni" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Inoltre, procedere ti farà uscire dalla sessione corrente, richiedendoti di accedere. Se attivata, dovrai completare la verifica in due passaggi di nuovo. Le sessioni attive su altri dispositivi potrebbero continuare a rimanere attive per un massimo di un'ora." }, + "newDeviceLoginProtection": { + "message": "Accesso nuovo dispositivo" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Disattiva la nuova protezione di accesso del dispositivo" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Attiva la nuova protezione di accesso del dispositivo" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Procedi qui sotto per disattivare le e-mail di verifica che Bitwarden invia quando accedi da un nuovo dispositivo." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Procedi qui sotto per far sì che Bitwarden invii e-mail di verifica quando accedi da un nuovo dispositivo." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Con la nuova protezione di accesso del dispositivo disattivata, chiunque con la tua parola d'accesso principale può accedere al tuo account da qualsiasi dispositivo. Per proteggere il tuo account senza e-mail di verifica, imposta l'accesso in due passaggi." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Modifiche alla protezione del nuovo dispositivo salvate" + }, "sessionsDeauthorized": { "message": "Tutte le sessioni revocate" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Gestisci" }, - "canManage": { - "message": "Può gestire" + "manageCollection": { + "message": "Gestisci collezione" + }, + "viewItems": { + "message": "Vedi voci" + }, + "viewItemsHidePass": { + "message": "Vedi elementi, parole d'accesso nascoste" + }, + "editItems": { + "message": "Modifica voci" + }, + "editItemsHidePass": { + "message": "Modifica elementi, parole d'accesso nascoste" }, "disable": { "message": "Disattiva" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Si è verificato un problema durante la lettura della chiave di sicurezza. Riprova." }, - "twoFactorWebAuthnWarning": { - "message": "A causa di limitazioni della piattaforma, WebAuthn non può essere utilizzato su tutte le app Bitwarden. Dovresti abilitare un altro metodo di verifica in due passaggi per accedere al tuo account anche quando WebAuthn non può essere usato. Piattaforme supportate:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Cassaforte Web ed estensione per il browser desktop/laptop con un browser abilitato per WebAuthn (Chrome, Opera, Vivaldi o Firefox con FIDO U2F abilitato)." + "twoFactorWebAuthnWarning1": { + "message": "A causa delle limitazioni della piattaforma, WebAuthn non può essere usato in tutte le applicazioni Bitwarden. È necessario impostare un altro fornitore d'accesso in due passaggi in modo da poter accedere al tuo account quando WebAuthn non può essere usato." }, "twoFactorRecoveryYourCode": { "message": "Il tuo codice di recupero Bitwarden per la verifica in due passaggi" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Hai ancora 1 invito." }, + "inviteZeroEmailDesc": { + "message": "Hai 0 inviti rimanenti." + }, "userUsingTwoStep": { "message": "Questo utente usa la verifica in due passaggi per proteggere il suo account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "SSO non collegato." + }, "unlinkedSsoUser": { "message": "SSO per l'utente $ID$ scollegato.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Di fiducia" }, + "needsApproval": { + "message": "Necessita approvazione" + }, + "areYouTryingtoLogin": { + "message": "Stai tentando di accedere?" + }, + "logInAttemptBy": { + "message": "Tentativo di accesso di $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Tipo di dispositivo" + }, + "ipAddress": { + "message": "Indirizzo IP" + }, + "confirmLogIn": { + "message": "Conferma accesso" + }, + "denyLogIn": { + "message": "Nega accesso" + }, + "thisRequestIsNoLongerValid": { + "message": "La richiesta non è più valida." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Accesso confermato per $EMAIL$ con $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Hai negato un tentativo di accesso da un altro dispositivo. Se eri davvero tu, prova di nuovo ad accedere con il dispositivo." + }, + "loginRequestHasAlreadyExpired": { + "message": "La richiesta di accesso è già scaduta." + }, + "justNow": { + "message": "Proprio ora" + }, + "requestedXMinutesAgo": { + "message": "Richiesto $MINUTES$ minuti fa", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creazione account su" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Imposta requisiti minimi di complessità per il generatore di password." }, - "passwordGeneratorPolicyInEffect": { - "message": "Una o più politiche dell'organizzazione stanno influenzando le impostazioni del tuo generatore." - }, "masterPasswordPolicyInEffect": { "message": "Una o più politiche dell'organizzazione richiedono che la tua password principale soddisfi questi requisiti:" }, @@ -6554,15 +6717,6 @@ "message": "Generatore", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Cosa vuoi generare?" - }, - "passwordType": { - "message": "Tipo di password" - }, - "regenerateUsername": { - "message": "Rigenera nome utente" - }, "generateUsername": { "message": "Genera nome utente" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Tipo di nome utente" - }, "plusAddressedEmail": { "message": "Indirizzo email alternativo", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Usa la casella di posta catch-all di dominio." }, + "useThisEmail": { + "message": "Usa questa e-mail" + }, "random": { "message": "Casuale", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Nome host", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token di accesso API" - }, "deviceVerification": { "message": "Verifica del dispositivo" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Nessuna raccolta" }, - "canView": { - "message": "Può visualizzare" - }, - "canViewExceptPass": { - "message": "Può visualizzare, eccetto le password" - }, - "canEdit": { - "message": "Può modificare" - }, - "canEditExceptPass": { - "message": "Può modificare, eccetto le password" - }, "noCollectionsAdded": { "message": "Nessuna raccolta aggiunta" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Dispositivi fidati" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Una volta autenticati, i membri decrittograferanno i dati della cassaforte usando una chiave memorizzata sul proprio dispositivo. La", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "I membri non avranno bisogno di una parola d'accesso principale durante l'accesso con SSO. La parola d'accesso principale è rimpiazzata da una chiave di crittografia memorizzata nel dispositivo, rendendo il dispositivo attendibile. Il primo dispositivo in cui un membro crea i propri account e accesso sarà attendibile. I nuovi dispositivi dovranno essere approvati da un dispositivo fidato esistente o da un amministratore. La", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "politica organizzazione unica", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": ", la", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "politica SSO obbligatorio", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": ", e la", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "politica amministrazione del recupero dell'account", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "con registrazione automatica si attiverà quando questa opzione è usata.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Le autorizzazioni della tua organizzazione sono state aggiornate, obbligandoti a impostare una password principale.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limita l'eliminazione di elementi ai membri con il permesso di gestione" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Proprietari e amministratori possono gestire tutte le raccolte e gli elementi" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Dominio alias" - }, "alreadyHaveAccount": { "message": "Hai già un account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Non hai accesso alla gestione di questa raccolta." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Autorizzazione Può gestire mancante" + "grantManageCollectionWarningTitle": { + "message": "Permessi di gestione raccolte mancanti" }, - "grantAddAccessCollectionWarning": { - "message": "Concedi l'autorizzazione Può gestire per consentire la gestione completa della raccolta, inclusa l'eliminazione della raccolta." + "grantManageCollectionWarning": { + "message": "Concedi i permessi di gestione della collezione per consentire la gestione completa della raccolta, inclusa l'eliminazione della raccolta." }, "grantCollectionAccess": { "message": "Consenti a gruppi o membri di accedere a questa raccolta." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "mese per membro" }, + "monthPerMemberBilledAnnually": { + "message": "mese per membro fatturato annualmente" + }, "seats": { "message": "Slot" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Chiave privata" + }, + "sshPublicKey": { + "message": "Chiave pubblica" + }, + "sshFingerprint": { + "message": "Impronta digitale" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "Non puoi rimuovere raccolte con i soli permessi di visualizzazione: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Gli amministratori ora hanno la possibilità di eliminare gli account membri che appartengono a un dominio rivendicato." + }, + "deleteManagedUserWarningDesc": { + "message": "Questa azione eliminerà l'account membro includendo tutti gli elementi nella sua cassaforte. Questo sostituisce l'azione precedente Rimuovi." + }, + "deleteManagedUserWarning": { + "message": "Eliminare è una nuova azione!" + }, + "seatsRemaining": { + "message": "Hai $REMAINING$ posti di $TOTAL$ posti assegnati a quest'organizzazione. Contatta il provider per gestire l'abbonamento.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Organizzazione esistente" + }, + "selectOrganizationProviderPortal": { + "message": "Seleziona un'organizzazione da aggiungere al tuo portale fornitori." + }, + "noOrganizations": { + "message": "Non ci sono organizzazioni da elencare" + }, + "yourProviderSubscriptionCredit": { + "message": "Il tuo abbonamento fornitore riceverà un credito per qualsiasi tempo rimanente nell'abbonamento dell'organizzazione." + }, + "doYouWantToAddThisOrg": { + "message": "Vuoi aggiungere questa organizzazione a $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Aggiunta organizzazione esistente" + }, + "assignedExceedsAvailable": { + "message": "I posti assegnati superano i posti disponibili." } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 44213d143aa9..dbe3ceb6eaf3 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "リスクがあるメンバー" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "合計メンバー数" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "合計アプリ数" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "このアイテムのタイプは何ですか?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "ベースドメイン", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "アイテム名" }, - "cannotRemoveViewOnlyCollections": { - "message": "表示のみの権限が与えられているコレクションを削除することはできません: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "例:", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "本人確認" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "ログイン開始" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "送信" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "デバイスに通知を送信しました。" }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "お使いのデバイスに通知が送信されました" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "アカウントがロック解除されていることと、フィンガープリントフレーズが他の端末と一致していることを確認してください" - }, "versionNumber": { "message": "バージョン $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "あいまいな文字を避ける", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "パスワードの再生成" - }, "length": { "message": "長さ" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "危険な操作" }, - "dangerZoneDesc": { - "message": "これらの操作はやり直せないため注意してください!" - }, - "dangerZoneDescSingular": { - "message": "この操作は元に戻せないため注意してください!" - }, "deauthorizeSessions": { "message": "セッションの承認を取り消す" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "次に進むと現在のセッションからログアウトし二段階認証を含め再度ログインが必要になります。他のデバイスでのセッションは1時間程度維持されます。" }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "全てのセッションを無効化" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "管理" }, - "canManage": { - "message": "管理可能" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "無効化" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "セキュリティキーの読み取り中に問題が発生しました。もう一度やり直して下さい。" }, - "twoFactorWebAuthnWarning": { - "message": "プラットフォームの制限により、WebAuthnはBitwardenの全てのアプリケーションで使用できるわけではありません。WebAuthnが使用できない場合に備えて、他の二段階認証プロバイダを有効化しておくことをおすすめします。サポートされているプラットフォーム:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "WebAuthn対応ブラウザ(FIDO U2F が有効な Chrome、Opera、Vivaldi、Firefox)を搭載したデスクトップ/ノートパソコンで、WebVaultとブラウザ拡張機能を使用します。" + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "二段階認証のリカバリーコード" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "このユーザーはアカウントを保護するため二段階認証を利用しています。" }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "ユーザー $ID$ にリンクされていない SSO", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "アカウント作成:" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "パスワード生成の最低要件を設定する。" }, - "passwordGeneratorPolicyInEffect": { - "message": "組織の要件がパスワード生成の設定に影響しています。" - }, "masterPasswordPolicyInEffect": { "message": "組織が求めるマスターパスワードの要件は:" }, @@ -6554,15 +6717,6 @@ "message": "ジェネレーター", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "何を生成しますか?" - }, - "passwordType": { - "message": "パスワードの種類" - }, - "regenerateUsername": { - "message": "ユーザー名を再生成" - }, "generateUsername": { "message": "ユーザー名を生成" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "ユーザー名の種類" - }, "plusAddressedEmail": { "message": "プラス付きのメールアドレス", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "ドメインに設定されたキャッチオール受信トレイを使用します。" }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "ランダム", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "ホスト名", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API アクセストークン" - }, "deviceVerification": { "message": "デバイス認証" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "コレクションなし" }, - "canView": { - "message": "閲覧可能" - }, - "canViewExceptPass": { - "message": "パスワード以外は閲覧可能" - }, - "canEdit": { - "message": "編集可能" - }, - "canEditExceptPass": { - "message": "パスワード以外は編集可能" - }, "noCollectionsAdded": { "message": "コレクションが追加されていません" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "信頼できるデバイス" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "認証されると、メンバーはデバイスに保存されたキーを使って保管庫のデータを復号します。", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "単一組織", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "ポリシー", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO 必須", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "ポリシーと", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "アカウント回復の管理", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "自動登録のポリシーがオンになります。", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "組織の権限が更新され、マスターパスワードの設定が必要になりました。", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "コレクションの削除を所有者と管理者のみに制限" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "所有者と管理者はすべてのコレクションとアイテムを管理できます" }, @@ -8544,9 +8686,6 @@ "message": "自己ホスト型サーバー URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "エイリアスドメイン" - }, "alreadyHaveAccount": { "message": "既にアカウントをお持ちですか?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "このコレクションを管理する権限がありません。" }, - "grantAddAccessCollectionWarningTitle": { - "message": "「管理可能」権限がありません" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "コレクションの削除を含む完全なコレクション管理を許可するには「管理可能」権限を許可してください。" + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "グループまたはメンバーにこのコレクションへのアクセスを許可します。" @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "月/メンバーあたり" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "シート" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "表示のみの権限が与えられているコレクションを削除することはできません: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 610bc0f6d873..92c64db7f561 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Რა სახის საგანია ეს?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "დომენის ბაზა", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "მაგ.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "ავტორიზაცია დაწყებულია" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "გადაცემა" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "შეტყობინება გამოიგზავნა თქვენი ტელეფონის მისამართით." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "ვერსია $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index e6cce3405d52..7e6f614b9c8b 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 215bde966ad7..83d0f5f6b893 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "ಇದು ಯಾವ ರೀತಿಯ ಐಟಂ?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "ಮೂಲ ಡೊಮೇನ್", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ಉದಾಹರಣೆ.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "ಒಪ್ಪಿಸು" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "ಆವೃತ್ತಿ $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಪುನರುತ್ಪಾದಿಸಿ" - }, "length": { "message": "ಉದ್ದ" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "ಅಪಾಯ ವಲಯ" }, - "dangerZoneDesc": { - "message": "ಎಚ್ಚರಿಕೆಯಿಂದ, ಈ ಕ್ರಿಯೆಗಳು ಹಿಂತಿರುಗಿಸಲಾಗುವುದಿಲ್ಲ!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "ಸೆಷನ್‌ಗಳನ್ನು ಅನಧಿಕೃತಗೊಳಿಸಿ" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "ಮುಂದುವರಿಯುವುದರಿಂದ ನಿಮ್ಮ ಪ್ರಸ್ತುತ ಸೆಷನ್‌ನಿಂದ ನಿಮ್ಮನ್ನು ಲಾಗ್ಔಟ್ ಮಾಡುತ್ತದೆ, ನಿಮಗೆ ಮತ್ತೆ ಲಾಗ್ ಇನ್ ಆಗುವ ಅಗತ್ಯವಿರುತ್ತದೆ. ಸಕ್ರಿಯಗೊಳಿಸಿದ್ದರೆ ಮತ್ತೆ ಎರಡು-ಹಂತದ ಲಾಗಿನ್‌ಗೆ ನಿಮ್ಮನ್ನು ಕೇಳಲಾಗುತ್ತದೆ. ಇತರ ಸಾಧನಗಳಲ್ಲಿ ಸಕ್ರಿಯ ಸೆಷನ್‌ಗಳು ಒಂದು ಗಂಟೆಯವರೆಗೆ ಸಕ್ರಿಯವಾಗಿ ಮುಂದುವರಿಯಬಹುದು." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "ಎಲ್ಲಾ ಸೆಷನ್‌ಗಳು ಅನಧಿಕೃತವಾಗಿವೆ" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "ವ್ಯವಸ್ಥಾಪಕ" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "ನಿಷ್‌ಕ್ರಿಯೆಗೊಳಿಸಿ" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "ಭದ್ರತಾ ಕೀಲಿಯನ್ನು ಓದುವಲ್ಲಿ ಸಮಸ್ಯೆ ಇದೆ. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸು." }, - "twoFactorWebAuthnWarning": { - "message": "ಪ್ಲಾಟ್‌ಫಾರ್ಮ್ ಮಿತಿಗಳ ಕಾರಣ, ಎಲ್ಲಾ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಲ್ಲಿ ವೆಬ್‌ಆಥ್ನ್ ಅನ್ನು ಬಳಸಲಾಗುವುದಿಲ್ಲ. ನೀವು ಇನ್ನೊಂದು ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಒದಗಿಸುವವರನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬೇಕು ಇದರಿಂದ ವೆಬ್‌ಆಥ್ನ್ ಅನ್ನು ಬಳಸಲಾಗದಿದ್ದಾಗ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಪ್ರವೇಶಿಸಬಹುದು. ಬೆಂಬಲಿತ ವೇದಿಕೆಗಳು:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "ವೆಬ್‌ಆಥ್ನ್ ಶಕ್ತಗೊಂಡ ಬ್ರೌಸರ್‌ನೊಂದಿಗೆ ಡೆಸ್ಕ್‌ಟಾಪ್ / ಲ್ಯಾಪ್‌ಟಾಪ್‌ನಲ್ಲಿ ವೆಬ್ ವಾಲ್ಟ್ ಮತ್ತು ಬ್ರೌಸರ್ ವಿಸ್ತರಣೆಗಳು (ಕ್ರೋಮ್, ಒಪೇರಾ, ವಿವಾಲ್ಡಿ, ಅಥವಾ ಎಫ್‌ಐಡಿಒ ಯು 2 ಎಫ್ ಸಕ್ರಿಯಗೊಳಿಸಿದ ಫೈರ್‌ಫಾಕ್ಸ್)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "ನಿಮ್ಮ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಮರುಪಡೆಯುವಿಕೆ ಕೋಡ್" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "ಈ ಬಳಕೆದಾರರು ತಮ್ಮ ಖಾತೆಯನ್ನು ರಕ್ಷಿಸಲು ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಅನ್ನು ಬಳಸುತ್ತಿದ್ದಾರೆ." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "ಬಳಕೆದಾರ $ID$ ಗಾಗಿ ಲಿಂಕ್ ಮಾಡದ SSO.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "ಪಾಸ್ವರ್ಡ್ ಜನರೇಟರ್ ಕಾನ್ಫಿಗರೇಶನ್ಗಾಗಿ ಕನಿಷ್ಠ ಅವಶ್ಯಕತೆಗಳನ್ನು ಹೊಂದಿಸಿ." }, - "passwordGeneratorPolicyInEffect": { - "message": "ಒಂದು ಅಥವಾ ಹೆಚ್ಚಿನ ಸಂಸ್ಥೆ ನೀತಿಗಳು ನಿಮ್ಮ ಜನರೇಟರ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳ ಮೇಲೆ ಪರಿಣಾಮ ಬೀರುತ್ತವೆ." - }, "masterPasswordPolicyInEffect": { "message": "ಒಂದು ಅಥವಾ ಹೆಚ್ಚಿನ ಸಂಸ್ಥೆ ನೀತಿಗಳಿಗೆ ಈ ಕೆಳಗಿನ ಅವಶ್ಯಕತೆಗಳನ್ನು ಪೂರೈಸಲು ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯವಿದೆ:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 0cd1302ea2f0..7ff621bf3c15 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "항목의 유형이 무엇입니까?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "기본 도메인", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "예)", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "보내기" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "기기에 알림이 전송되었습니다." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "버전 $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "비밀번호 재생성" - }, "length": { "message": "길이" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "위험 구역" }, - "dangerZoneDesc": { - "message": "주의, 이 행동들은 되돌릴 수 없음!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "세션 해제" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "계속 진행하면, 현재 세션 또한 로그아웃 되므로 다시 로그인하여야 합니다. 2단계 로그인이 활성화 된 경우 다시 요구하는 메세지가 표시됩니다. 다른 기기의 활성화 된 세션은 최대 1시간 동안 유지 될 수 있습니다." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "모든 세션 해제 됨" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "관리" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "비활성화" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "보안 키를 읽어오는데 문제가 발생했습니다. 다시 시도해보십시오." }, - "twoFactorWebAuthnWarning": { - "message": "플랫폼 제한으로 인해, 모든 Bitwarden 애플리케이션에서 WebAuthn을 사용할 수 없습니다. WebAuthn을 사용할 수 없을 때 계정에 접근할 수 있도록 다른 2단계 로그인 방법을 활성화하십시오. 지원하는 플랫폼:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "WebAuthn 지원 브라우저가 있는 데스크탑/랩탑의 웹 보관함 및 브라우저 확장 (WebAuthn이 활성화된 Chrome, Opera, Vivaldi 또는 Firefox 사용)" + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Bitwarden 2단계 로그인 복구 코드" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "이 사용자는 계정을 보호하기 위해 2단계 로그인을 사용하고 있습니다." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "$ID$ 사용자에 대한 SSO 연결이 해제되었습니다.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "비밀번호 생성기 설정에 최소 요구 사항을 설정해주세요." }, - "passwordGeneratorPolicyInEffect": { - "message": "하나 이상의 단체 정책이 생성기 설정에 영항을 미치고 있습니다." - }, "masterPasswordPolicyInEffect": { "message": "하나 이상의 단체 정책이 마스터 비밀번호가 다음 사항을 따르도록 요구합니다:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 51ca6a0f798a..d20d76394879 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Riskam pakļautie dalībnieki" }, + "atRiskMembersWithCount": { + "message": "Riskam pakļautie dalībnieki ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Riskam pakļautas lietotnes ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Šie dalībnieki piesakās lietotnēs ar vājām, atklātām vai atkārtoti izmantotām parolēm." + }, + "atRiskApplicationsDescription": { + "message": "Šīm lietotnēm ir vājas, atklātas vai atkārtoti izmantotas paroles." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Šie dalībnieki piesakās $APPNAME$ ar vājām, atklātām vai atkārtoti izmantotām parolēm.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Kopējais dalībnieku skaits" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Kopējais lietotņu skaits" }, + "unmarkAsCriticalApp": { + "message": "Noņemt kritiskas lietontes atzīmi" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritiskas lietotnes atzīme sekmīgi noņemta" + }, "whatTypeOfItem": { "message": "Kāda veida vienums tas ir?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Vai tiešām neatgriezeniski izdzēst šo mapi?" + }, "baseDomain": { "message": "Pamata domēns", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Vienuma nosaukums" }, - "cannotRemoveViewOnlyCollections": { - "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "piem.,", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Jāapliecina sava identitāte" }, + "weDontRecognizeThisDevice": { + "message": "Mēs neatpazīstam šo ierīci. Jāievada kods, kas tika nosūtīts e-pastā, lai apliecinātu savu identitāti." + }, + "continueLoggingIn": { + "message": "Turpināt pieteikšanos" + }, "whatIsADevice": { "message": "Kas ir ierīce?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Uzsākta pieteikšanās" }, + "logInRequestSent": { + "message": "Pieprasījums nosūtīts" + }, "submit": { "message": "Iesniegt" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Uz ierīci ir nosūtīts paziņojums." }, + "notificationSentDevicePart1": { + "message": "Bitwarden jāatslēdz savā ierīcē vai " + }, + "areYouTryingToAccessYourAccount": { + "message": "Vai mēģini piekļūt savam kontam?" + }, + "accessAttemptBy": { + "message": "$EMAIL$ piekļuves mēģinājums", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Apstiprināt piekļuvi" + }, + "denyAccess": { + "message": "Noraidīt piekļuvi" + }, + "notificationSentDeviceAnchor": { + "message": "tīmekļa lietotnē" + }, + "notificationSentDevicePart2": { + "message": "Pirms apstiprināšanas jāpārliecinās, ka pirkstu nospieduma vārdkopa atbilst zemāk esošajai." + }, + "notificationSentDeviceComplete": { + "message": "Savā ierīcē jāatslēdz Bitwarden. Pirms apstiprināšanas jāpārliecinās, ka pirkstu nospieduma vārdkopa atbilst zemāk esošajai." + }, "aNotificationWasSentToYourDevice": { "message": "Uz ierīci tika nosūtīts paziņojums" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Lūgums pārliecināties, ka konts ir atslēgts un atpazīšanas vārdkopa ir tāda pati arī otrā ierīcē" - }, "versionNumber": { "message": "Laidiens $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Izvairīties no viegli sajaucamām rakstzīmēm", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Pārizveidot paroli" - }, "length": { "message": "Garums" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Bīstamā sadaļa" }, - "dangerZoneDesc": { - "message": "Piesardzību, šīs darbības nav atsaucamas!" - }, - "dangerZoneDescSingular": { - "message": "Uzmanīgi, šī darbība ir neatgriezeniska!" - }, "deauthorizeSessions": { "message": "Padarīt sesijas spēkā neesošas" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Tiks veikta atteikšanās no pašreizējās sesijas, un pēc tam būs nepieciešams atkārtoti pieteikties. Būs nepieciešama arī divpakāpju pieteikšanās, ja tā ir iespējota. Citās ierīcēs darbojošās sesijas var būt spēkā līdz vienai stundai." }, + "newDeviceLoginProtection": { + "message": "Pieteikšanās jaunā ierīcē" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Visu sesiju darbība ir atsaukta" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Pārvaldīt" }, - "canManage": { - "message": "Var pārvaldīt" + "manageCollection": { + "message": "Pārvaldīt krājumu" + }, + "viewItems": { + "message": "Apskatīt vienumus" + }, + "viewItemsHidePass": { + "message": "Apskatīt vienumus, paslēptās paroles" + }, + "editItems": { + "message": "Labot vienumus" + }, + "editItemsHidePass": { + "message": "Labot vienumus, paslēptās paroles" }, "disable": { "message": "Atspējot" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Radās sarežģījumi, nolasot drošības atslēgu. Jāmēģina vēlreiz." }, - "twoFactorWebAuthnWarning": { - "message": "Platformas ierobežojumu dēļ WebAuth nevar izmantot visās Bitwarden lietotnēs. Ir ieteicams iespējot vēl kādu divpakāpju pieteikšanās nodrošinātāju, lai varētu piekļūt kontam, kad nav iespējams izmantot WebAuth. Atbalstītās platformas:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Tīmekļa glabātava un pārlūku paplašinājums galddatorā/klēpjdatorā ar WebAuthn iespējotu pārlūku (Chrome, Opera, Vivaldi vai Firefox ar iespējotu FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Platformas ierobežojumu dēļ WebAuth nevar izmantot visās Bitwarden lietotnēs. Ir ieteicams iespējot vēl kādu divpakāpju pieteikšanās nodrošinātāju, lai varētu piekļūt kontam, kad nav iespējams izmantot WebAuth." }, "twoFactorRecoveryYourCode": { "message": "Bitwarden divpakāpju pieteikšanās atkopšanas kods" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Tev ir atlicis 1 uzaicinājums." }, + "inviteZeroEmailDesc": { + "message": "Tev ir atlikuši 0 uzaicinājumu." + }, "userUsingTwoStep": { "message": "Šis lietotājs izmanto divpakāpju pieteikšanos, lai aizsargātu savu kontu." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "SSO atsaistīta." + }, "unlinkedSsoUser": { "message": "Atsaistīta vienotā pieteikšanās lietotājam $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Uzticama" }, + "needsApproval": { + "message": "Nepieciešams apstiprinājums" + }, + "areYouTryingtoLogin": { + "message": "Vai mēģini pieteikties?" + }, + "logInAttemptBy": { + "message": "$EMAIL$ pieteikšanās mēģinājums", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Ierīces veids" + }, + "ipAddress": { + "message": "IP adrese" + }, + "confirmLogIn": { + "message": "Apstiprināt pieteikšanos" + }, + "denyLogIn": { + "message": "Atteikt pieteikšanos" + }, + "thisRequestIsNoLongerValid": { + "message": "Šis pieprasījums vairs nav derīgs." + }, + "logInConfirmedForEmailOnDevice": { + "message": "$EMAIL$ pieteikšanās apstiprināta ierīcē $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Tu noraidīji pieteikšanās mēģinājumu no citas ierīces. Ja tas tiešām biji Tu, mēģini pieteikties no ierīces vēlreiz!" + }, + "loginRequestHasAlreadyExpired": { + "message": "Pieteikšanās pieprasījuma derīgums jau ir beidzies." + }, + "justNow": { + "message": "Tikko" + }, + "requestedXMinutesAgo": { + "message": "Pieprasīts pirms $MINUTES$ minūtēm", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Tiek veidots konts" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Uzstādīt paroļu veidotāja uzstādījumu mazākās izpildāmās prasības." }, - "passwordGeneratorPolicyInEffect": { - "message": "Viens vai vairāki apvienības nosacījumi ietekmē veidotāja iestatījumus." - }, "masterPasswordPolicyInEffect": { "message": "Vienā vai vairākos apvienības nosacījumos ir norādīts, ka galvenajai parolei ir jāatbilst šādām prasībām:" }, @@ -6554,15 +6717,6 @@ "message": "Veidotājs", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Ko ir vēlme izveidot?" - }, - "passwordType": { - "message": "Paroles veids" - }, - "regenerateUsername": { - "message": "Pārizveidot lietotājvārdu" - }, "generateUsername": { "message": "Izveidot lietotājvārdu" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Lietotājvārda veids" - }, "plusAddressedEmail": { "message": "E-pasta adrese ar plusu", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Izmantot uzstādīto domēna visu tverošo iesūtni." }, + "useThisEmail": { + "message": "Izmantot šo e-pasta adresi" + }, "random": { "message": "Nejauši", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Resursdatora nosaukums", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API piekļuves pilnvara" - }, "deviceVerification": { "message": "Ierīces apstiprināšana" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Nav krājuma" }, - "canView": { - "message": "Var skatīt" - }, - "canViewExceptPass": { - "message": "Var skatīt, izņemot paroles" - }, - "canEdit": { - "message": "Var labot" - }, - "canEditExceptPass": { - "message": "Var labot, izņemot paroles" - }, "noCollectionsAdded": { "message": "Nav pievienotu krājumu" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Uzticamās ierīces" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Pēc pieteikšanās dalībnieki atšifrēs glabātavas saturu ar ierīcē glabātu atslēgu. ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Dalībniekiem nebūs nepieciešama galvenā parole, kad pieteiksies ar SSO. Galvenā parole ir aizvietota ar šifrēšanas atslēgu, kas tiek glabāta ierīcē, padarot to par uzticamu. Pirmā ierīce, kurā dalībnieks izveido savu kontu un tajā piesakās, būs uzticama. Jaunas ierīces būs nepieciešams apstiprināt ar esošu uzticamo ierīci vai pārvaldītājam. ", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "Vienas apvienības", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "nosacījums,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO nepieciešamības", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "nosacījums un", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "kontu atkopšanas pārvaldības", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "nosacījums ar automātisku ievietošanu sarakstā tiks ieslēgts, kad šī iespēja tiek izmantota.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "nosacījums tiks ieslēgts, kad šī iespēja tiks izmantota.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Apvienības atļaujas tika atjauninātas, un tās pieprasa iestatīt galveno paroli.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Ļaut krājumu izdzēšanu tikai īpašniekiem un pārvaldītājiem" }, + "limitItemDeletionDesc": { + "message": "Ierobežot lietotājiem vienumu izdzēšanu ar atļauju \"Var pārvaldīt\"" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Īpašnieki un pārvaldnieki var pārvaldīt visus krājumus un vienumus" }, @@ -8544,9 +8686,6 @@ "message": "Pašmitināta servera URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Aizstājdomēns" - }, "alreadyHaveAccount": { "message": "Jau ir konts?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Nav piekļuves pārvaldīt šo krājumu." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Trūkst atļaujas \"Var pārvaldīt\"" + "grantManageCollectionWarningTitle": { + "message": "Trūkst krājumu pārvaldīšanas atļaujas" }, - "grantAddAccessCollectionWarning": { - "message": "Jānodrošina atļauja \"Var pārvaldīt\", lai ļautu pilnu krājuma pārvaldību, tajā skaitā krājuma izdzēšanu." + "grantManageCollectionWarning": { + "message": "Jānodrošina atļauja \"Pārvaldīt kŗajumu\", lai ļautu pilnu krājuma pārvaldību, tajā skaitā krājuma izdzēšanu." }, "grantCollectionAccess": { "message": "Piešķirt kopām vai dalībniekiem piekļuvi šim krājumam." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "mēnesī par dalībnieku" }, + "monthPerMemberBilledAnnually": { + "message": "par dalībnieku mēnesī ar ikgadēju rēķinu" + }, "seats": { "message": "Vietas" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Atslēgas algoritms" }, + "sshPrivateKey": { + "message": "Privātā atslēga" + }, + "sshPublicKey": { + "message": "Publiskā atslēga" + }, + "sshFingerprint": { + "message": "Pirkstu nospiedums" + }, "sshKeyFingerprint": { "message": "Pirkstu nospiedums" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Apraksta kods" }, + "cannotRemoveViewOnlyCollections": { + "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Svarīgs paziņojums" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Apvienības nosaukums nevar pārsniegt 50 rakstzīmes." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Abonements drīz tiks atjaunots. Lai nodrošinātu nepārtrauktu pakalpojumu, pirms $RENEWAL_DATE$ jāsazinās ar $RESELLER$, lai apstiprinātu atjaunošanu.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Rēķins par abonementu tika izdots $ISSUED_DATE$. Lai nodrošinātu nepārtrauktu pakalpojumu, pirms $DUE_DATE$ jāsazinās ar $RESELLER$, lai apstiprinātu atjaunošanu.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Rēķins par abonementu nav apmaksāts. Lai nodrošinātu nepārtrauktu pakalpojumu, pirms $GRACE_PERIOD_END$ jāsazināš ar $RESELLER$, lai apstiprinātu atjaunošanu.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Pārvaldītājiem tagad ir iespēja izdzēst dalībnieku kontus, kas pieder pieteiktam domēnam." + }, + "deleteManagedUserWarningDesc": { + "message": "Šī darbība izdzēsīs dalībnieka kontu, tajā skaitā visus glabātavas vienumus. Tā aizstāj iepriekšējo darbību \"Noņemt\"." + }, + "deleteManagedUserWarning": { + "message": "\"Izdzēst\" ir jaunā darbība." + }, + "seatsRemaining": { + "message": "Ir atlikušas $REMAINING$ no apvienībai piesaistītajām $TOTAL$ vietām. Jāsazinās ar nodrošinātāju, lai pārvaldītu savu abonementu.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Esoša apvienība" + }, + "selectOrganizationProviderPortal": { + "message": "Jāatlasa apvienība, kuru pievienot savam nodrošinātāju portālam." + }, + "noOrganizations": { + "message": "Nav apvienību, ko uzskaitīt" + }, + "yourProviderSubscriptionCredit": { + "message": "Nodrošinātāja abonements saņemts kredītu par jebkuru atlikušo laiku apvienības abonementā." + }, + "doYouWantToAddThisOrg": { + "message": "Vai šo apvienību pievienot $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Pievienota esoša apvienība" + }, + "assignedExceedsAvailable": { + "message": "Piešķirtās vietas pārsniedz pieejamās vietas." } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index a6eb0e474fd3..617f3b2d8ace 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "ഇത് ഏതു തരം ഇനം ആണ്?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "അടിസ്ഥാന ഡൊമെയ്ൻ", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ഉദാഹരണം.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "സമർപ്പിക്കുക" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "വേർഷൻ $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "പാസ്സ്‌വേഡ് വീണ്ടും സൃഷ്ടിക്കുക" - }, "length": { "message": "ദൈര്‍ഘ്യം" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "അപകട മേഖല" }, - "dangerZoneDesc": { - "message": "ശ്രദ്ധിക്കുക, ഈ പ്രവർത്തനങ്ങൾ മാറ്റാനാവില്ല!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize Sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if enabled. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "എല്ലാ സെഷനും നിരസിച്ചു." }, @@ -2110,8 +2200,20 @@ "manage": { "message": "നിയന്ത്രിക്കുക" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "പ്രവര്‍ത്തന രഹിതമാക്കുക" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "ഈ ഉപയോക്താവ് അവരുടെ അക്കൗണ്ട് രണ്ട്-പ്രവേശനം ഉപയോഗിച്ച് സുരക്ഷിതമാക്കിയിരിക്കുന്നു." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "പാസ്‌വേഡ് ജനറേറ്റർ കോൺഫിഗറേഷനായി മിനിമം ആവശ്യകതകൾ സജ്ജമാക്കുക." }, - "passwordGeneratorPolicyInEffect": { - "message": "ഒന്നോ അതിലധികമോ സംഘടന നയങ്ങൾ നിങ്ങളുടെ പാസ്സ്‌വേഡ് സൃഷ്ടാവിൻ്റെ ക്രമീകരണങ്ങളെ ബാധിക്കുന്നു." - }, "masterPasswordPolicyInEffect": { "message": "ഒന്നോ അതിലധികമോ ഓർഗനൈസേഷൻ നയങ്ങൾക്ക് ഇനിപ്പറയുന്ന ആവശ്യകതകൾ നിറവേറ്റുന്നതിന് നിങ്ങളുടെ മാസ്റ്റർ പാസ്‌വേഡ് ആവശ്യമാണ്:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index e6cce3405d52..7e6f614b9c8b 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index e6cce3405d52..7e6f614b9c8b 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index da80546c9a5c..a804f5dfafb4 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -96,7 +96,7 @@ "message": "Apps marked as critical" }, "application": { - "message": "Application" + "message": "Program" }, "atRiskPasswords": { "message": "At-risk passwords" @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Hva slags element er dette?" }, @@ -169,22 +208,22 @@ "message": "Kortholderens navn" }, "loginCredentials": { - "message": "Login credentials" + "message": "Legitimasjoner for innlogging" }, "personalDetails": { - "message": "Personal details" + "message": "Personlige detaljer" }, "identification": { - "message": "Identification" + "message": "Identifikasjon" }, "contactInfo": { - "message": "Contact info" + "message": "Kontaktinfo" }, "cardDetails": { - "message": "Card details" + "message": "Kortdetaljer" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$-detaljer", "placeholders": { "brand": { "content": "$1", @@ -193,19 +232,19 @@ } }, "itemHistory": { - "message": "Item history" + "message": "Gjenstandshistorikk" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Autentiseringsnøkkel" }, "autofillOptions": { - "message": "Autofill options" + "message": "Autoutfyllings-innstillinger" }, "websiteUri": { - "message": "Website (URI)" + "message": "Nettsted (URİ)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "Nettsted (URİ) $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": { @@ -215,16 +254,16 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Nettsted lagt til" }, "addWebsite": { - "message": "Add website" + "message": "Legg til nettsted" }, "deleteWebsite": { - "message": "Delete website" + "message": "Slett nettsted" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Standard ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -345,7 +384,7 @@ "message": "Dr․" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Utløpt kort" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" @@ -369,7 +408,7 @@ "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" + "message": "Lær mer om autentisering" }, "folder": { "message": "Mappe" @@ -393,17 +432,17 @@ "message": "Boolsk verdi" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Avkryssingsboks" }, "cfTypeLinked": { "message": "Tilknyttet", "description": "This describes a field that is 'linked' (related) to another field." }, "fieldType": { - "message": "Field type" + "message": "Felttype" }, "fieldLabel": { - "message": "Field label" + "message": "Feltetikett" }, "remove": { "message": "Fjern" @@ -416,7 +455,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Du", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -425,6 +464,18 @@ "editFolder": { "message": "Rediger mappen" }, + "newFolder": { + "message": "Ny mappe" + }, + "folderName": { + "message": "Mappenavn" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Grunndomene", "description": "Domain name. Example: website.com" @@ -469,7 +520,7 @@ "message": "Generer et passord" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Generér passordfrase" }, "checkPassword": { "message": "Sjekk om passordet har blitt utsatt." @@ -572,7 +623,7 @@ "message": "Sikkert notat" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH-nøkkel" }, "typeLoginPlural": { "message": "Innlogginger" @@ -605,7 +656,7 @@ "message": "Fullt navn" }, "address": { - "message": "Address" + "message": "Adresse" }, "address1": { "message": "Adresse 1" @@ -650,7 +701,7 @@ "message": "Vis elementet" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Ny $TYPE$", "placeholders": { "type": { "content": "$1", @@ -659,7 +710,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Rediger $TYPE$", "placeholders": { "type": { "content": "$1", @@ -684,19 +735,10 @@ "message": "Element" }, "itemDetails": { - "message": "Item details" + "message": "Gjenstandens detaljer" }, "itemName": { - "message": "Item name" - }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } + "message": "Gjenstandens navn" }, "ex": { "message": "f.eks.", @@ -722,7 +764,7 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "Kopiering lyktes" }, "copyValue": { "message": "Kopier verdien", @@ -737,7 +779,7 @@ "description": "Copy passphrase to clipboard" }, "passwordCopied": { - "message": "Password copied" + "message": "Passordet er kopiert" }, "copyUsername": { "message": "Kopier brukernavnet", @@ -756,7 +798,7 @@ "description": "Copy URI to clipboard" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiér $FIELD$", "placeholders": { "field": { "content": "$1", @@ -765,34 +807,34 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiér nettsted" }, "copyNotes": { - "message": "Copy notes" + "message": "Kopiér notater" }, "copyAddress": { - "message": "Copy address" + "message": "Kopiér adresse" }, "copyPhone": { - "message": "Copy phone" + "message": "Kopiér telefonnummer" }, "copyEmail": { "message": "Copy email" }, "copyCompany": { - "message": "Copy company" + "message": "Kopiér firma" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Kopiér fødselsnummer" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiér passnummer" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Kopiér lisensnummer" }, "copyName": { - "message": "Copy name" + "message": "Kopiér navn" }, "me": { "message": "Meg" @@ -883,7 +925,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "Gjenstandene ble flyttet til $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -892,7 +934,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "Gjenstanden ble flyttet til $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -952,22 +994,22 @@ "message": "Logget av" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Du har blitt logget ut av kontoen din." }, "loginExpired": { "message": "Din innloggingsøkt har utløpt." }, "restartRegistration": { - "message": "Restart registration" + "message": "Start registreringen på nytt" }, "expiredLink": { - "message": "Expired link" + "message": "Utløpt lenke" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Du har kanskje allerede en konto" }, "logOutConfirmation": { "message": "Er du sikker på at du vil logge av?" @@ -994,7 +1036,7 @@ "message": "Logg på med enhet må settes opp i Bitwarden-innstillingene. Trenger du et annet alternativ?" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Trenger du et annet alternativ?" }, "loginWithMasterPassword": { "message": "Logg på med hovedpassord" @@ -1009,13 +1051,13 @@ "message": "Bruk en annen innloggingsmetode" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Logg inn med passnøkkel" }, "useSingleSignOn": { "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Velkommen tilbake" }, "invalidPasskeyPleaseTryAgain": { "message": "Ugyldig Passkey. Vennligst prøv igjen." @@ -1099,10 +1141,10 @@ "message": "Opprett en konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Er du ny til Bitwarden?" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Velg et sterkt passord" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -1117,7 +1159,7 @@ "message": "Logg på" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Logg inn på Bitwarden" }, "authenticationTimeout": { "message": "Authentication timeout" @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Fortsett innloggingen" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Pålogging startet" }, + "logInRequestSent": { + "message": "Forespørsel sendt" + }, "submit": { "message": "Send inn" }, @@ -1190,13 +1241,13 @@ "message": "Innstillinger" }, "accountEmail": { - "message": "Account email" + "message": "Kontoens E-postadresse" }, "requestHint": { - "message": "Request hint" + "message": "Be om et hint" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Be om passordhint" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "message": "Enter your account email address and your password hint will be sent to you" @@ -1239,10 +1290,10 @@ "message": "Din nye konto har blitt opprettet! Du kan nå logge på." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "Den nye kontoen din er opprettet!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Du har blitt logget inn!" }, "trialAccountCreated": { "message": "Kontoen ble opprettet." @@ -1260,10 +1311,10 @@ "message": "E-postadresse" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Hvelvet ditt er låst" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Kontoen din er låst" }, "uuid": { "message": "UUID" @@ -1326,11 +1377,38 @@ "notificationSentDevice": { "message": "Et varsel har blitt sendt til enheten din." }, - "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "nett-app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, + "aNotificationWasSentToYourDevice": { + "message": "Et varsel ble sendt til enheten din" }, "versionNumber": { "message": "Versjon $VERSION_NUMBER$", @@ -1435,7 +1513,7 @@ "message": "E-post" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Skriv inn koden du har fått tilsendt på E-post." }, "continue": { "message": "Fortsett" @@ -1616,17 +1694,14 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Unngå forvekslingsbare tegn", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerer passord" - }, "length": { "message": "Lengde" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Minimum passordlengde" }, "uppercase": { "message": "Store bokstaver (A-Z)", @@ -1664,10 +1739,10 @@ "message": "Passordhistorikk" }, "generatorHistory": { - "message": "Generator history" + "message": "Generatorhistorikk" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Tøm generatorhistorikk" }, "cleargGeneratorHistoryDescription": { "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" @@ -1676,13 +1751,13 @@ "message": "Det er ingen passord å liste opp." }, "clearHistory": { - "message": "Clear history" + "message": "Tøm historikk" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Ingenting å vise" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Du har ikke generert noe i det siste" }, "clear": { "message": "Tøm", @@ -1722,7 +1797,7 @@ "message": "Vennligst logg på igjen." }, "currentSession": { - "message": "Current session" + "message": "Gjeldende økt" }, "requestPending": { "message": "Request pending" @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Faresone" }, - "dangerZoneDesc": { - "message": "Vær forsiktig, disse handlingene kan ikke reverseres!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Fjern autorisering av økter" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Å fortsette vil også logge deg av din nåværende økt, og gjør at du vil måtte logge på igjen. Du vil også bli bedt om 2-trinnsinnlogging igjen, dersom det er aktivert. Aktive økter på andre enheter kan kanskje forbli aktive i opptil en time." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Fjernet autoriseringen fra alle økter" }, @@ -1899,7 +1989,7 @@ "message": "Dataene har blitt vellykket importert inn i hvelvet ditt." }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "$AMOUNT$ gjenstander totalt ble importert.", "placeholders": { "amount": { "content": "$1", @@ -1929,16 +2019,16 @@ "message": "Feil under dekryptering av den eksporterte filen. Krypteringsnøkkelen samsvarte ikke med krypteringsnøkkelen som ble brukt eksport av data." }, "destination": { - "message": "Destination" + "message": "Destinasjon" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Lær mer om importalternativene dine" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Velg en mappe" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Velg en samling" }, "importTargetHint": { "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", @@ -1951,7 +2041,7 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Filen inneholder utildelte elementer." }, "selectFormat": { "message": "Velg formatet til importfilen" @@ -2110,8 +2200,20 @@ "manage": { "message": "Behandle" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Deaktiver" @@ -2150,7 +2252,7 @@ "message": "You are leaving Bitwarden and launching an external website in a new window." }, "twoStepContinueToBitwardenUrlTitle": { - "message": "Continue to bitwarden.com?" + "message": "Vil du fortsette til bitwarden.com?" }, "twoStepContinueToBitwardenUrlDesc": { "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." @@ -2165,7 +2267,7 @@ "message": "Nøkkel" }, "twoStepAuthenticatorEnterCodeV2": { - "message": "Verification code" + "message": "Verifiseringskode" }, "twoStepAuthenticatorReaddDesc": { "message": "Dersom du trenger å legge den til til en annen enhet, er QR-koden (eller -nøkkelen) som kreves av din autentiseringsapp nedenfor." @@ -2249,7 +2351,7 @@ "message": "Client Id" }, "twoFactorDuoClientSecret": { - "message": "Client Secret" + "message": "Klienthemmelighet" }, "twoFactorDuoApiHostname": { "message": "API-vertsnavn" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Det oppsto et problem med å lese sikkerhetsnøkkelen. Prøv igjen." }, - "twoFactorWebAuthnWarning": { - "message": "På grunn av plattformbegrensninger, kan ikke WebAuthn brukes på alle Bitwarden-apper. Du bør aktivere en annen 2-trinnsinnloggingsleverandør, slik at du kan få tilgang til kontoen din når WebAuthn ikke kan brukes. Støttede plattformer:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Netthvelv og nettleserutvidelser, på en datamaskin med en WebAuthn støttende nettleser (Chrome, Opera, Vivaldi, eller Firefox med FIDO U2F aktivert)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Din 2-trinnsinnloggingsgjenopprettingskode for Bitwarden" @@ -2893,7 +2992,7 @@ "message": "Ta kontakt med kundestøtte" }, "contactSupportShort": { - "message": "Contact Support" + "message": "Kontakt kundestøtte" }, "updatedPaymentMethod": { "message": "Oppdaterte betalingsmetoden." @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Denne brukeren bruker 2-trinnsinnlogging til å beskytte kontoen sin." }, @@ -3357,7 +3459,7 @@ "message": "Netthvelv" }, "cli": { - "message": "CLI" + "message": "Ledetekst" }, "bitWebVault": { "message": "Bitwarden Web vault" @@ -3387,13 +3489,13 @@ "message": "Innloggingsforsøket mislyktes grunnet feil 2-trinnsinnlogging." }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Feil passord" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Feil kode" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Feil PIN-kode" }, "pin": { "message": "PIN", @@ -3445,7 +3547,7 @@ } }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Vis alle påloggingsalternativer" }, "viewAllLoginOptions": { "message": "Vis alle påloggingsalternativer" @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Koblet fra SSO for brukeren $ID$.", "placeholders": { @@ -3787,28 +3892,89 @@ "message": "First login" }, "trusted": { - "message": "Trusted" + "message": "Betrodd" + }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Prøver du å logge på?" + }, + "logInAttemptBy": { + "message": "Påloggingsforsøk av $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP-adresse" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Akkurat nå" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Oppretter en konto på" }, "checkYourEmail": { - "message": "Check your email" + "message": "Sjekk E-postinnboksen din" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Følg lenken i E-postadressen som ble sendt til" }, "andContinueCreatingYourAccount": { "message": "and continue creating your account." }, "noEmail": { - "message": "No email?" + "message": "Ingen E-post?" }, "goBack": { - "message": "Go back" + "message": "Gå tilbake" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "for å redigere E-postadressen din." }, "view": { "message": "Vis" @@ -3892,7 +4058,7 @@ "message": "Din E-postadresse har blitt bekreftet." }, "emailVerifiedV2": { - "message": "Email verified" + "message": "E-post bekreftet" }, "emailVerifiedFailed": { "message": "Klarte ikke å bekrefte E-postadressen din. Prøv å sende en ny bekreftelses-E-post." @@ -4349,10 +4515,10 @@ "message": "Unsubscribe" }, "atAnyTime": { - "message": "at any time." + "message": "når som helst." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Ved å fortsette, samtykker du til" }, "and": { "message": "and" @@ -4376,7 +4542,7 @@ "message": "Pause for hvelvet" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Tidsavbrudd" }, "vaultTimeoutDesc": { "message": "Velg når hvelvet ditt skal ta pause og utføre den valgte handlingen." @@ -4445,7 +4611,7 @@ "message": "Valgt" }, "recommended": { - "message": "Recommended" + "message": "Anbefalt" }, "ownership": { "message": "Eierskap" @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Sett minimumskrav for konfigurasjon av passordgenerator." }, - "passwordGeneratorPolicyInEffect": { - "message": "En eller flere av organisasjonens vilkår påvirker generatorinnstillingene dine." - }, "masterPasswordPolicyInEffect": { "message": "En eller flere av organisasjonens vilkår krever hovedpassordet ditt for å oppfylle følgende krav:" }, @@ -4761,13 +4924,13 @@ "message": "Du kan nå lukke denne fanen og fortsette i utvidelsen." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Du har vellykket logget inn" }, "thisWindowWillCloseIn5Seconds": { "message": "This window will automatically close in 5 seconds" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Du kan lukke dette vinduet" }, "includeAllTeamsFeatures": { "message": "Alle Lag funksjoner, plus:" @@ -4932,7 +5095,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copyLink": { - "message": "Copy link" + "message": "Kopier lenke" }, "copySendLink": { "message": "Kopier Send-lenke", @@ -5680,7 +5843,7 @@ "message": "Feil" }, "decryptionError": { - "message": "Decryption error" + "message": "Dekrypteringsfeil" }, "couldNotDecryptVaultItemsBelow": { "message": "Bitwarden could not decrypt the vault item(s) listed below." @@ -5690,7 +5853,7 @@ "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "for å unngå ytterligere datatap.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -6357,7 +6520,7 @@ "message": "Rotasjon av faktureringssynkroniserings-token vil gjøre den forrige token ugyldig." }, "selfHostedServer": { - "message": "self-hosted" + "message": "selvbetjent" }, "customEnvironment": { "message": "Custom environment" @@ -6554,23 +6717,14 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Hva vil du generere?" - }, - "passwordType": { - "message": "Passordtype" - }, - "regenerateUsername": { - "message": "Regenerer Brukernavn" - }, "generateUsername": { "message": "Generer brukernavn" }, "generateEmail": { - "message": "Generate email" + "message": "Generér E-post" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Verdien må være mellom $MIN$ og $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -6584,7 +6738,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Bruk minst $RECOMMENDED$ tegn for å generere et sterkt passord.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Brukernavntype" - }, "plusAddressedEmail": { "message": "Pluss-adressert e-post", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Bruk domenets konfigurerte catch-all innboks." }, + "useThisEmail": { + "message": "Bruk denne E-postadressen" + }, "random": { "message": "Vilkårlig", "description": "Generates domain-based username using random letters" @@ -6627,23 +6781,23 @@ "message": "Vilkårlig ord" }, "usernameGenerator": { - "message": "Username generator" + "message": "Brukernavngenerator" }, "useThisPassword": { - "message": "Use this password" + "message": "Bruk dette passordet" }, "useThisUsername": { - "message": "Use this username" + "message": "Bruk dette brukernavnet" }, "securePasswordGenerated": { "message": "Secure password generated! Don't forget to also update your password on the website." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "Bruk denne 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": "to create a strong unique password", + "message": "for å lage et sterkt og unikt passord", "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'" }, "service": { @@ -6709,11 +6863,11 @@ "message": "Generer et e-postalias med en ekstern videresendingstjeneste." }, "forwarderDomainName": { - "message": "Email domain", + "message": "E-postdomene", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Velg et domene som støttes av den valgte tjenesten", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -6731,11 +6885,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Generert av Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Nettsted: $WEBSITE$. Generert av Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -6745,7 +6899,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Ugyldig $SERVICENAME$-API-sjetong", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -6755,7 +6909,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Ugyldig $SERVICENAME$-API-sjetong: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -6779,7 +6933,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Ugyldig $SERVICENAME$-domene.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -6799,7 +6953,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Ukjent $SERVICENAME$-feil oppstod.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -6822,9 +6976,6 @@ "message": "Vertsnavn", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API tilgangstoken" - }, "deviceVerification": { "message": "Enhetsverifisering" }, @@ -6941,7 +7092,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Inndataverdien må være minst $MIN$.", "placeholders": { "min": { "content": "$1", @@ -6950,7 +7101,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Inndataverdien kan ikke være mer enn $MAX$.", "placeholders": { "max": { "content": "$1", @@ -6980,10 +7131,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 felt trenger din oppmerksomhet." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ felter trenger din oppmerksomhet.", "placeholders": { "count": { "content": "$1", @@ -7001,7 +7152,7 @@ "message": "Duo two-step login is required for your account." }, "launchDuo": { - "message": "Launch Duo" + "message": "Start Duo" }, "turnOn": { "message": "Slå på" @@ -7010,7 +7161,7 @@ "message": "På" }, "off": { - "message": "Off" + "message": "Av" }, "members": { "message": "Medlemmer" @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Ingen samling" }, - "canView": { - "message": "Kan vise" - }, - "canViewExceptPass": { - "message": "Kan vise, bortsett fra passord" - }, - "canEdit": { - "message": "Kan redigere" - }, - "canEditExceptPass": { - "message": "Kan redigere, bortsett fra passord" - }, "noCollectionsAdded": { "message": "Ingen samlinger lagt til" }, @@ -7696,37 +7835,37 @@ "message": "Verification required for this action. Set a PIN to continue." }, "setPin": { - "message": "Set PIN" + "message": "Velg PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Bekreft med biometri" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Avventer bekreftelse" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Kunne ikke fullføre biometri." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Trenger du en annen metode?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Bruk hovedpassord" }, "usePin": { - "message": "Use PIN" + "message": "Bruk PIN-kode" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Bruk biometri" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Skriv inn bekreftelseskoden som ble sendt til e-postadressen din." }, "resendCode": { - "message": "Resend code" + "message": "Send koden på nytt" }, "memberColumnHeader": { - "message": "Member" + "message": "Medlem" }, "groupSlashMemberColumnHeader": { "message": "Group/Member" @@ -7840,7 +7979,7 @@ "message": "Filopplasting" }, "upload": { - "message": "Upload" + "message": "Last opp" }, "acceptedFormats": { "message": "Aksepterte formater:" @@ -7852,13 +7991,13 @@ "message": "eller" }, "unlockWithBiometrics": { - "message": "Unlock with biometrics" + "message": "Lås opp med biometri" }, "unlockWithPin": { - "message": "Unlock with PIN" + "message": "Lås opp med PIN-kode" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Lås opp med hovedpassord" }, "licenseAndBillingManagement": { "message": "Håndtering av lisens og fakturering" @@ -7954,10 +8093,10 @@ "message": "This user can access Secrets Manager" }, "important": { - "message": "Important:" + "message": "Viktig:" }, "viewAll": { - "message": "View all" + "message": "Vis alle" }, "showingPortionOfTotal": { "message": "Showing $PORTION$ of $TOTAL$", @@ -7973,10 +8112,10 @@ } }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Fiks feilene nedenfor og prøv igjen." }, "description": { - "message": "Description" + "message": "Beskrivelse" }, "errorReadingImportFile": { "message": "An error occurred when trying to read the import file" @@ -7995,7 +8134,7 @@ "description": "Software Development Kit" }, "createAnAccount": { - "message": "Create an account" + "message": "Opprett en konto" }, "createSecret": { "message": "Create a secret" @@ -8017,7 +8156,7 @@ "message": "Import secrets" }, "getStarted": { - "message": "Get started" + "message": "Kom i gang" }, "complete": { "message": "$COMPLETED$/$TOTAL$ Complete", @@ -8136,7 +8275,7 @@ "message": "Update KDF settings" }, "loginInitiated": { - "message": "Login initiated" + "message": "Innlogging igangsatt" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { "message": "Remember this device to make future logins seamless" @@ -8145,25 +8284,25 @@ "message": "Device approval required. Select an approval option below:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Enhetsgodkjennelse kreves" }, "selectAnApprovalOptionBelow": { "message": "Select an approval option below" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Husk denne enheten" }, "uncheckIfPublicDevice": { "message": "Uncheck if using a public device" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Godkjenn fra en av dine andre enheter" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Be om administratorgodkjennelse" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Godkjenn med hovedpassord" }, "trustedDeviceEncryption": { "message": "Trusted device encryption" @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8208,7 +8347,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "av $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -8226,7 +8365,7 @@ } }, "verificationRequired": { - "message": "Verification required", + "message": "Verifisering kreves", "description": "Default title for the user verification dialog." }, "recoverAccount": { @@ -8263,7 +8402,7 @@ "message": "Device info" }, "time": { - "message": "Time" + "message": "Tid" }, "denyAllRequests": { "message": "Deny all requests" @@ -8362,7 +8501,7 @@ } }, "next": { - "message": "Next" + "message": "Neste" }, "ssoLoginIsRequired": { "message": "SSO login is required" @@ -8377,16 +8516,16 @@ "message": "Admin approval requested" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Forespørselen din har blitt sendt til administratoren din." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Du vil bli varslet når det er godkjent." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Har du problemer med å logge inn?" }, "loginApproved": { - "message": "Login approved" + "message": "Innlogging godkjent" }, "userEmailMissing": { "message": "User email missing" @@ -8395,18 +8534,18 @@ "message": "Active user email not found. Logging you out." }, "deviceTrusted": { - "message": "Device trusted" + "message": "Enheten er betrodd" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "Ingen aktive Send", "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.", + "message": "Bruk Send til å dele kryptert informasjon med noen.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inviteUsers": { - "message": "Invite Users" + "message": "Inviter brukere" }, "secretsManagerForPlan": { "message": "Secrets Manager for $PLAN$", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8526,7 +8668,7 @@ "message": "Max potential service account cost" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Innlogget!" }, "beta": { "message": "Beta" @@ -8538,32 +8680,29 @@ "message": "Edited collections" }, "baseUrl": { - "message": "Server URL" + "message": "Tjener-URL" }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Har du allerede en konto?" }, "toggleSideNavigation": { "message": "Toggle side navigation" }, "skipToContent": { - "message": "Skip to content" + "message": "Hopp frem til innholdet" }, "managePermissionRequired": { "message": "At least one member or group must have can manage permission." }, "typePasskey": { - "message": "Passkey" + "message": "Passnøkkel" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Passkoden vil ikke bli kopiert" }, "passkeyNotCopiedAlert": { "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" @@ -8578,7 +8717,7 @@ } }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Se detaljerte instruksjoner på hjelpesidene våre på", "description": "This is followed a by a hyperlink to the help website." }, "installBrowserExtension": { @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -8627,7 +8766,7 @@ "message": "Service account access updated" }, "commonImportFormats": { - "message": "Common formats", + "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { @@ -8704,7 +8843,7 @@ "message": "Provider Portal" }, "success": { - "message": "Success" + "message": "Suksess" }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." @@ -8713,10 +8852,10 @@ "message": "You cannot add yourself to collections." }, "assign": { - "message": "Assign" + "message": "Knytt" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Legg til i samlinger" }, "assignToTheseCollections": { "message": "Assign to these collections" @@ -8728,7 +8867,7 @@ "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Velg samlinger å tilordne" }, "noCollectionsAssigned": { "message": "No collections have been assigned" @@ -8750,25 +8889,25 @@ } }, "addField": { - "message": "Add field" + "message": "Legg til felt" }, "editField": { - "message": "Edit field" + "message": "Rediger felt" }, "items": { - "message": "Items" + "message": "Gjenstander" }, "assignedSeats": { "message": "Assigned seats" }, "assigned": { - "message": "Assigned" + "message": "Tildelt" }, "used": { - "message": "Used" + "message": "Brukt" }, "remaining": { - "message": "Remaining" + "message": "Gjenstår" }, "unlinkOrganization": { "message": "Unlink organization" @@ -8796,11 +8935,11 @@ "description": "A subscription status label." }, "pastDue": { - "message": "Past due", + "message": "Forbi måldatoen", "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "Abonnement har utløpt", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { @@ -9056,7 +9195,7 @@ "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integreringer" }, "integrationsDesc": { "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." @@ -9068,7 +9207,7 @@ "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." }, "ssoDescStart": { - "message": "Configure", + "message": "Sett opp", "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, "ssoDescEnd": { @@ -9082,7 +9221,7 @@ "message": "SCIM" }, "scimIntegrationDescStart": { - "message": "Configure ", + "message": "Sett opp ", "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, "scimIntegrationDescEnd": { @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9219,7 +9361,7 @@ "message": "Enter your Enterprise organization information" }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Vis gjenstander i $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -9229,7 +9371,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Tilbake til $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -9239,11 +9381,11 @@ } }, "back": { - "message": "Back", + "message": "Tilbake", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Fjern $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -9337,7 +9479,7 @@ "message": "Unverified" }, "verified": { - "message": "Verified" + "message": "Verifisert" }, "viewSecret": { "message": "View secret" @@ -9356,7 +9498,7 @@ "message": "Quickly view member access across the organization by upgrading to an Enterprise plan." }, "date": { - "message": "Date" + "message": "Dato" }, "exportClientReport": { "message": "Export client report" @@ -9383,13 +9525,13 @@ "message": "On" }, "memberAccessReportTwoFactorEnabledFalse": { - "message": "Off" + "message": "Av" }, "memberAccessReportAuthenticationEnabledTrue": { "message": "On" }, "memberAccessReportAuthenticationEnabledFalse": { - "message": "Off" + "message": "Av" }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." @@ -9434,7 +9576,7 @@ "message": "This action will remove your access to this secret." }, "invoice": { - "message": "Invoice" + "message": "Faktura" }, "unassignedSeatsAvailable": { "message": "You have $SEATS$ unassigned seats available.", @@ -9461,7 +9603,7 @@ "message": "Client details" }, "downloadCSV": { - "message": "Download CSV" + "message": "Last ned CSV" }, "monthlySubscriptionUserSeatsMessage": { "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " @@ -9495,7 +9637,7 @@ "message": "Add to folder" }, "selectFolder": { - "message": "Select folder" + "message": "Velg mappe" }, "personalItemTransferWarningSingular": { "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." @@ -9547,13 +9689,13 @@ "message": "Project IDs" }, "projectId": { - "message": "Project ID" + "message": "Prosjekt-ID" }, "projectsAccessedByMachineAccount": { "message": "The following projects can be accessed by this machine account." }, "config": { - "message": "Config" + "message": "Oppsett" }, "learnMoreAboutEmergencyAccess": { "message": "Learn more about emergency access" @@ -9693,14 +9835,23 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Privat nøkkel" + }, + "sshPublicKey": { + "message": "Offentlig nøkkel" + }, + "sshFingerprint": { + "message": "Fingeravtrykk" + }, "sshKeyFingerprint": { - "message": "Fingerprint" + "message": "Fingeravtrykk" }, "sshKeyPrivateKey": { - "message": "Private key" + "message": "Privat nøkkel" }, "sshKeyPublicKey": { - "message": "Public key" + "message": "Offentlig nøkkel" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -9736,7 +9887,7 @@ "message": "Passwordless SSO" }, "accountRecovery": { - "message": "Account recovery" + "message": "Kontogjenoppretting" }, "customRoles": { "message": "Custom roles" @@ -9754,13 +9905,13 @@ "message": "Up to 20 machine accounts" }, "current": { - "message": "Current" + "message": "Nåværende" }, "secretsManagerSubscriptionInfo": { "message": "Your Secrets Manager subscription will upgrade based on the plan selected" }, "bitwardenPasswordManager": { - "message": "Bitwarden Password Manager" + "message": "Bitwarden passordbehandler" }, "secretsManagerComplimentaryPasswordManager": { "message": "Your complimentary one year Password Manager subscription will upgrade to the selected plan. You will not be charged until the complimentary period is over." @@ -9769,20 +9920,20 @@ "message": "File saved to device. Manage from your device downloads." }, "publicApi": { - "message": "Public API", + "message": "Offentlig API", "description": "The text, 'API', is an acronym and should not be translated." }, "showCharacterCount": { - "message": "Show character count" + "message": "Vis tegntelleren" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "Skjul tegntelleren" }, "editAccess": { "message": "Edit access" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "Bruk tekstfelter for data som sikkerhetsspørsmål" }, "hiddenHelpText": { "message": "Use hidden fields for sensitive data like a password" @@ -9797,15 +9948,15 @@ "message": "Enter the the field's html id, name, aria-label, or placeholder." }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Inkluder store bokstaver", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { - "message": "A-Z", + "message": "A-Å", "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Inkluder små bokstaver", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -9813,7 +9964,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Inkluder tall", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -9821,7 +9972,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Inkluder spesialtegn", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -9829,10 +9980,10 @@ "description": "Label for the password generator special characters checkbox" }, "addAttachment": { - "message": "Add attachment" + "message": "Legg til vedlegg" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Maksimal filstørrelse er 500 MB" }, "permanentlyDeleteAttachmentConfirmation": { "message": "Are you sure you want to permanently delete this attachment?" @@ -9946,11 +10097,20 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { - "message": "Important notice" + "message": "Viktig melding" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Sett opp 2-trinnspålogging" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." @@ -9959,7 +10119,7 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Minn meg på det senere" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -9977,16 +10137,16 @@ "message": "Yes, I can reliably access my email" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Slå på 2-trinnsinnlogging" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Endre kontoens E-postadresse" }, "removeMembers": { "message": "Remove members" }, "devices": { - "message": "Devices" + "message": "Enheter" }, "deviceListDescription": { "message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now." @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 7ebca5938e02..20ceae6f33b5 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "यो कस्तो प्रकारको वस्तु हो?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index b38e6f4c3c3a..09374b1cb026 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Leden in gevaar" }, + "atRiskMembersWithCount": { + "message": "Leden die risico lopen ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Applicaties die risico lopen ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Deze leden loggen in op toepassingen met zwakke, blootgestelde of hergebruikte wachtwoorden." + }, + "atRiskApplicationsDescription": { + "message": "Deze applicaties hebben zwakke, blootgelegde of hergebruikte wachtwoorden." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Deze leden loggen in op $APPNAME$ met zwakke, blootgestelde of hergebruikte wachtwoorden.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Totaal leden" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Totaal applicaties" }, + "unmarkAsCriticalApp": { + "message": "Markeren als belangrijke app ongedaan maken" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Markeren als belangrijke app ongedaan gemaakt" + }, "whatTypeOfItem": { "message": "Van welke categorie is dit item?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Weet je zeker dat je deze map definitief wilt verwijderen?" + }, "baseDomain": { "message": "Basisdomein", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Itemnaam" }, - "cannotRemoveViewOnlyCollections": { - "message": "Je kunt verzamelingen niet verwijderen met alleen rechten voor weergeven: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "bijv.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Controleer je identiteit" }, + "weDontRecognizeThisDevice": { + "message": "We herkennen dit apparaat niet. Voer de code in die naar je e-mail is verzonden om je identiteit te verifiëren." + }, + "continueLoggingIn": { + "message": "Doorgaan met inloggen" + }, "whatIsADevice": { "message": "Wat is een apparaat?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Inloggen gestart" }, + "logInRequestSent": { + "message": "Verzoek verzonden" + }, "submit": { "message": "Versturen" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Er is een bericht naar je apparaat verstuurd." }, + "notificationSentDevicePart1": { + "message": "Ontgrendel Bitwarden op je apparaat of op de " + }, + "areYouTryingToAccessYourAccount": { + "message": "Probeer je toegang te krijgen tot je account?" + }, + "accessAttemptBy": { + "message": "Inlogpoging door $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Toegang bevestigen" + }, + "denyAccess": { + "message": "Toegang weigeren" + }, + "notificationSentDeviceAnchor": { + "message": "web-app" + }, + "notificationSentDevicePart2": { + "message": "Zorg ervoor dat de Vingerafdrukzin overeenkomt met de onderstaande voor je deze goedkeurt." + }, + "notificationSentDeviceComplete": { + "message": "Ontgrendel Bitwarden op je apparaat. Zorg ervoor dat de vingerafdruk-zin overeenkomt met de onderstaande zin voordat je deze goedkeurt." + }, "aNotificationWasSentToYourDevice": { "message": "Er is een melding naar je apparaat verzonden" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Zorg ervoor dat je kluis is ontgrendeld en de vingerafdrukzin hetzelfde is op het andere apparaat" - }, "versionNumber": { "message": "Versie $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Dubbelzinnige tekens vermijden", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Opnieuw genereren" - }, "length": { "message": "Lengte" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Gevarenzone" }, - "dangerZoneDesc": { - "message": "Waarschuwing - deze acties zijn niet terug te draaien!" - }, - "dangerZoneDescSingular": { - "message": "Waarschuwing - deze actie is niet terug te draaien!" - }, "deauthorizeSessions": { "message": "Sessie-autorisaties intrekken" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Doorgaan zal je huidige sessie uitloggen, waarna je opnieuw moet inloggen. Je moet ook je tweestapsaanmelding opnieuw doorlopen, als die is ingeschakeld. Actieve sessies op andere apparaten blijven mogelijk nog een uur actief." }, + "newDeviceLoginProtection": { + "message": "Nieuwe apparaat login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Inlogbescherming nieuwe apparaten uitschakelen" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Inlogbescherming nieuwe apparaten inschakelen" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Ga hieronder verder voor het uitschakelen van de e-mailverificatieberichten die Bitwarden stuurt wanneer je inlogt vanaf een nieuw apparaat." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Ga hieronder verder om Bitwarden verificatie e-mails te sturen wanneer je inlogt vanaf een nieuw apparaat.\n\nGa hieronder verder om Bitwarden e-mailverificatieberichten te laten sturen wanneer je inlogt vanaf een nieuw apparaat." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Als je inlogbescherming voor nieuwe apparaten uitschakelt, kan iedereen op ieder apparaat met je hoofdwachtwoord inloggen. Stel tweestapsaanmelding in om je account te beschermen zonder e-mailverificatieberichten." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Wijzigingen inlogbescherming nieuwe apparaten opgeslagen" + }, "sessionsDeauthorized": { "message": "Autorisatie van alle sessies ingetrokken" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Beheren" }, - "canManage": { - "message": "Kan beheren" + "manageCollection": { + "message": "Collectie beheren" + }, + "viewItems": { + "message": "Items bekijken" + }, + "viewItemsHidePass": { + "message": "Items bekijken, verborgen wachtwoorden" + }, + "editItems": { + "message": "Items bewerken" + }, + "editItemsHidePass": { + "message": "Items bewerken, verborgen wachtwoorden" }, "disable": { "message": "Uitschakelen" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Er was een probleem met het lezen van de beveiligingssleutel. Probeer het nogmaals." }, - "twoFactorWebAuthnWarning": { - "message": "Vanwege platformbeperkingen kan WebAuthn niet in alle Bitwarden applicaties gebruikt worden. Stel een andere tweestapsaanmeldingsaanbieder in zodat je je account kunt benaderen wanneer WebAuthn niet beschikbaar is. De volgende platformen worden ondersteund:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Webkluis en browser-extensies op een desktop/laptop met een browser met ondersteuning voor WebAuthn (Chrome, Opera, Vivaldi of Firefox met FIDO U2F ingeschakeld)." + "twoFactorWebAuthnWarning1": { + "message": "Vanwege platformbeperkingen kan WebAuthn niet in alle Bitwarden-applicaties gebruikt worden. Stel een andere tweestapsaanmeldingsaanbieder in zodat je je account kunt benaderen wanneer WebAuthn niet beschikbaar is." }, "twoFactorRecoveryYourCode": { "message": "Je herstelcode voor Bitwarden-tweestapsaanmelding" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Je hebt 1 uitnodiging over." }, + "inviteZeroEmailDesc": { + "message": "Je hebt 0 uitnodigingen over." + }, "userUsingTwoStep": { "message": "Het account van deze gebruiker is beschermd met tweestapsaanmelding." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Ontkoppelde SSO." + }, "unlinkedSsoUser": { "message": "SSO ontkoppeld voor gebruiker $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Vertrouwd" }, + "needsApproval": { + "message": "Heeft goedkeuring nodig" + }, + "areYouTryingtoLogin": { + "message": "Probeer je in te loggen?" + }, + "logInAttemptBy": { + "message": "Inlogpoging door $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Apparaattype" + }, + "ipAddress": { + "message": "IP-adres" + }, + "confirmLogIn": { + "message": "Inloggen bevestigen" + }, + "denyLogIn": { + "message": "Inloggen afwijzen" + }, + "thisRequestIsNoLongerValid": { + "message": "Dit verzoek is niet langer geldig." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Inloggen voor $EMAIL$ bevestigd op $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Je hebt een inlogpoging vanaf een ander apparaat geweigerd. Als je dit toch echt zelf was, probeer dan opnieuw in te loggen met het apparaat." + }, + "loginRequestHasAlreadyExpired": { + "message": "Inlogverzoek is al verlopen." + }, + "justNow": { + "message": "Zojuist" + }, + "requestedXMinutesAgo": { + "message": "$MINUTES$ minuten geleden aangevraagd", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Account maken bij" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Stel minimale vereisten in voor de configuratie van het wachtwoord generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "Een of meer organisatiebeleidseisen heeft invloed op de instellingen van je generator." - }, "masterPasswordPolicyInEffect": { "message": "Een of meer organisatiebeleidseisen stelt de volgende eisen aan je hoofdwachtwoord:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Wat wil je genereren?" - }, - "passwordType": { - "message": "Type wachtwoord" - }, - "regenerateUsername": { - "message": "Gebruikersnaam opnieuw genereren" - }, "generateUsername": { "message": "Gebruikersnaam genereren" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Type gebruikersnaam" - }, "plusAddressedEmail": { "message": "E-mailadres-met-plus", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Gebruik de catch-all inbox van je domein." }, + "useThisEmail": { + "message": "Dit e-mailadres gebruiken" + }, "random": { "message": "Willekeurig", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostnaam", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API-toegangstoken" - }, "deviceVerification": { "message": "Apparaatverificatie" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Geen collectie" }, - "canView": { - "message": "Kan bekijken" - }, - "canViewExceptPass": { - "message": "Kan bekijken, behalve wachtwoorden" - }, - "canEdit": { - "message": "Kan bewerken" - }, - "canEditExceptPass": { - "message": "Kan bewerken, behalve wachtwoorden" - }, "noCollectionsAdded": { "message": "Geen collecties toegevoegd" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Vertrouwde apparaten" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Eenmaal ingelogd, ontsleutelen leden kluisgegevens met een op hun apparaat opgeslagen sleutel. Het", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Leden hebben geen hoofdwachtwoord nodig bij het inloggen met SSO. Het hoofdwachtwoord wordt vervangen door een encryptiesleutel die is opgeslagen op het apparaat, waardoor het apparaat is vertrouwd. Het eerste apparaat waarop een lid zijn/haar account aanmaakt en mee inlogt wordt vertrouwd. Nieuwe apparaten moeten worden goedgekeurd door een bestaand vertrouwde apparaat of door een beheerder. De", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "enkele organisatie", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "beleid,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO vereist", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "beleid en", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "accountherstel-administratie", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "beleid met automatische inschrijving wordt ingeschakeld wanneer je deze optie gebruikt.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "beleid zal worden ingeschakeld wanneer deze optie wordt gebruikt.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "De organisatierechten zijn bijgewerkt, je moet een hoofdwachtwoord instellen.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Verwijderen van collecties beperken tot eigenaren en managers" }, + "limitItemDeletionDesc": { + "message": "Beperk het verwijderen van items tot leden met het recht Kan beheren" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Eigenaren en beheerders kunnen alle collecties en items beheren" }, @@ -8544,9 +8686,6 @@ "message": "URL zelfgehoste server", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Aliasdomein" - }, "alreadyHaveAccount": { "message": "Heb je al een account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Je hebt geen toegang om deze collectie te beheren." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Ontbrekende Kan beheren machtigingen" + "grantManageCollectionWarningTitle": { + "message": "Recht voor het beheren van machtigingen ontbreekt" }, - "grantAddAccessCollectionWarning": { - "message": "Kan beheren machtigingen verlenen voor volledig verzamelingsbeheer, inclusief het verwijderen van verzamelingen." + "grantManageCollectionWarning": { + "message": "Beheren van machtigingen toekennen voor het toestaan van het volledig verzamelingenbeheer, inclusief het verwijderen van een verzameling." }, "grantCollectionAccess": { "message": "Groepen of mensen toegang tot deze collectie geven." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "maand per lid" }, + "monthPerMemberBilledAnnually": { + "message": "maand per lid jaarlijks gefactureerd" + }, "seats": { "message": "Personen" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Sleutelalgoritme" }, + "sshPrivateKey": { + "message": "Privé sleutel" + }, + "sshPublicKey": { + "message": "Publieke sleutel" + }, + "sshFingerprint": { + "message": "Vingerafdruk" + }, "sshKeyFingerprint": { "message": "Vingerafdruk" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Code bankafschrift" }, + "cannotRemoveViewOnlyCollections": { + "message": "Je kunt verzamelingen niet verwijderen met alleen rechten voor weergeven: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Belangrijke mededeling" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Organisatienaam mag niet langer zijn dan 50 tekens." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Je abonnement wordt binnenkort verlengd. Neem voor $RENEWAL_DATE$ contact op met $RESELLER$ om je verlenging te bevestigen en een ononderbroken service te verzekeren.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Er is een factuur voor je abonnement aangemaakt op $ISSUED_DATE$. Neem contact op met $RESELLER$ voor $DUE_DATE$ om je verlenging te bevestigen en een ononderbroken service te verzekeren.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "De factuur voor je abonnement is niet betaald. Neem contact op met $RESELLER$ voor $GRACE_PERIOD_END$ om je verlenging te bevestigen en een ononderbroken service te verzekeren.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Beheerders hebben nu de mogelijkheid om leden van een geclaimd domein te verwijderen." + }, + "deleteManagedUserWarningDesc": { + "message": "Deze actie verwijdert het account van het lid, inclusief alle items in hun kluis. Dit vervangt de vorige verwijderactie." + }, + "deleteManagedUserWarning": { + "message": "Verwijderen is een nieuwe actie!" + }, + "seatsRemaining": { + "message": "Je hebt nog $REMAINING$ plaatsen van de $TOTAL$ plaatsen die aan deze organisatie zijn toegewezen. Neem contact op met je provider om je abonnement te beheren.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Bestaande toevoegen" + }, + "selectOrganizationProviderPortal": { + "message": "Kies een organisatie om aan je providerportaal toe te voegen." + }, + "noOrganizations": { + "message": "Er zijn geen organisaties om weer te geven" + }, + "yourProviderSubscriptionCredit": { + "message": "Je providerabonnement zal een credit ontvangen voor de resterende tijd van het abonnement van de organisatie." + }, + "doYouWantToAddThisOrg": { + "message": "Wilt je deze organisatie toevoegen aan $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Bestaande organisatie toegevoegd" + }, + "assignedExceedsAvailable": { + "message": "Meer toegewezen dan beschikbare plaatsen." } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index cb6d070485d5..8b7682fa39c5 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Kva type oppføring er dette?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Rediger mappe" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Grunndomene", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "t.d.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Slå av" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index e6cce3405d52..7e6f614b9c8b 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index d063fa818a79..6935f334d62d 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -3,22 +3,22 @@ "message": "Wszystkie aplikacje" }, "criticalApplications": { - "message": "Critical applications" + "message": "Krytyczne aplikacje" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "Dostęp do informacji" }, "riskInsights": { - "message": "Risk Insights" + "message": "Spostrzeżenia dotyczące ryzyka" }, "passwordRisk": { - "message": "Password Risk" + "message": "Ryzyko związne z hasłem" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Przejrzyj hasła zagrożone (słabe, ujawnione lub ponownie używane) we wszystkich aplikacjach. Wybierz swoje najbardziej krytyczne aplikacje, aby nadać priorytet działaniom bezpieczeństwa swoim użytkownikom, aby zająć się hasłami zagrożonymi." }, "dataLastUpdated": { - "message": "Data last updated: $DATE$", + "message": "Data ostatniej aktualizacji: $DATE$", "placeholders": { "date": { "content": "$1", @@ -30,16 +30,16 @@ "message": "Powiadomieni członkowie" }, "revokeMembers": { - "message": "Revoke members" + "message": "Unieważnij członk" }, "restoreMembers": { - "message": "Restore members" + "message": "Przywróć członków" }, "cannotRestoreAccessError": { - "message": "Cannot restore organization access" + "message": "Nie można przywrócić dostępu organizacji" }, "allApplicationsWithCount": { - "message": "All applications ($COUNT$)", + "message": "Wszystkie aplikacje ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -48,10 +48,10 @@ } }, "createNewLoginItem": { - "message": "Create new login item" + "message": "Utwórz nowy element logowania" }, "criticalApplicationsWithCount": { - "message": "Critical applications ($COUNT$)", + "message": "Aplikacje krytyczne ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -60,7 +60,7 @@ } }, "notifiedMembersWithCount": { - "message": "Notified members ($COUNT$)", + "message": "Powiadomieni członkowie ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -69,7 +69,7 @@ } }, "noAppsInOrgTitle": { - "message": "No applications found in $ORG NAME$", + "message": "Nie znaleziono aplikacji w $ORG NAME$", "placeholders": { "org name": { "content": "$1", @@ -78,49 +78,88 @@ } }, "noAppsInOrgDescription": { - "message": "As users save logins, applications appear here, showing any at-risk passwords. Mark critical apps and notify users to update passwords." + "message": "Jako użytkownicy zapisują logowania, pojawiają się tutaj aplikacje, pokazujące wszelkie hasła zagrożone. Zaznacz kluczowe aplikacje i powiadamiaj użytkowników o potrzebie aktualizacji haseł." }, "noCriticalAppsTitle": { - "message": "You haven't marked any applications as a Critical" + "message": "Nie oznaczyłeś żadnych aplikacji jako krytycznych" }, "noCriticalAppsDescription": { - "message": "Select your most critical applications to discover at-risk passwords, and notify users to change those passwords." + "message": "Wybierz najbardziej krytyczne aplikacje, aby odkryć hasła zagrożone i poinformuj użytkowników, by zmienili te hasła." }, "markCriticalApps": { - "message": "Mark critical apps" + "message": "Oznacz krytyczne aplikacje" }, "markAppAsCritical": { - "message": "Mark app as critical" + "message": "Oznacz aplikację jako krytyczną" }, "appsMarkedAsCritical": { - "message": "Apps marked as critical" + "message": "Aplikacje oznaczone jako krytyczne" }, "application": { "message": "Aplikacja" }, "atRiskPasswords": { - "message": "At-risk passwords" + "message": "Zagrożone hasła" }, "requestPasswordChange": { - "message": "Request password change" + "message": "Poproś o zmianę hasła" }, "totalPasswords": { - "message": "Total passwords" + "message": "Wszystkie hasła" }, "searchApps": { - "message": "Search applications" + "message": "Wyszukaj aplikacje" }, "atRiskMembers": { - "message": "At-risk members" + "message": "Zagrożeni użytkownicy" + }, + "atRiskMembersWithCount": { + "message": "Zagrożeni członkowie ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Zagrożone aplikacje ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Ci członkowie logują się do aplikacji ze słabymi, ujawnionymi lub ponownie używanymi hasłami." + }, + "atRiskApplicationsDescription": { + "message": "Te aplikacje mają słabe, ujawnione lub ponownie użyte hasła." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Ci użytkownicy logują się do $APPNAME$ ze słabymi, ujawnionymi lub ponownie używanymi hasłami.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } }, "totalMembers": { - "message": "Total members" + "message": "Wszyscy członkowie" }, "atRiskApplications": { - "message": "At-risk applications" + "message": "Zagrożone aplikacje" }, "totalApplications": { - "message": "Total applications" + "message": "Wszystkie aplikacje" + }, + "unmarkAsCriticalApp": { + "message": "Odznacz jako krytyczną aplikację" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Krytyczna aplikacja została pomyślnie odznaczona" }, "whatTypeOfItem": { "message": "Jakiego rodzaju jest to element?" @@ -425,6 +464,18 @@ "editFolder": { "message": "Edytuj folder" }, + "newFolder": { + "message": "Nowy folder" + }, + "folderName": { + "message": "Nazwa folderu" + }, + "folderHintText": { + "message": "Zagnieżdżaj foldery dodając nazwę folderu nadrzędnego, a następnie “/”. Przykład: Społeczne/Fora" + }, + "deleteFolderPermanently": { + "message": "Czy na pewno chcesz trwale usunąć ten folder?" + }, "baseDomain": { "message": "Domena podstawowa", "description": "Domain name. Example: website.com" @@ -469,7 +520,7 @@ "message": "Wygeneruj hasło" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Wygeneruj hasło wyrazowe" }, "checkPassword": { "message": "Sprawdź, czy hasło zostało ujawnione." @@ -572,7 +623,7 @@ "message": "Bezpieczna notatka" }, "typeSshKey": { - "message": "SSH key" + "message": "Klucz SSH" }, "typeLoginPlural": { "message": "Dane logowania" @@ -668,7 +719,7 @@ } }, "viewItemType": { - "message": "Zobacz $TYPE$", + "message": "Zobacz $ITEMTYPE$", "placeholders": { "itemtype": { "content": "$1", @@ -689,15 +740,6 @@ "itemName": { "message": "Nazwa elementu" }, - "cannotRemoveViewOnlyCollections": { - "message": "Nie można usunąć kolekcji z uprawnieniami tylko do przeglądania: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "np.", "description": "Short abbreviation for 'example'." @@ -733,11 +775,11 @@ "description": "Copy password to clipboard" }, "copyPassphrase": { - "message": "Copy passphrase", + "message": "Skopiuj hasło wyrazowe", "description": "Copy passphrase to clipboard" }, "passwordCopied": { - "message": "Password copied" + "message": "Hasło zostało skopiowane" }, "copyUsername": { "message": "Kopiuj nazwę użytkownika", @@ -994,7 +1036,7 @@ "message": "Logowanie za pomocą urządzenia musi być włączone w ustawieniach aplikacji Bitwarden. Potrzebujesz innej opcji?" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Potrzebujesz innego sposobu?" }, "loginWithMasterPassword": { "message": "Logowanie hasłem głównym" @@ -1009,13 +1051,13 @@ "message": "Użyj innej metody logowania" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Logowanie się za pomocą Passkey" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Użyj jednokrotnego logowania" }, "welcomeBack": { - "message": "Welcome back" + "message": "Witaj ponownie" }, "invalidPasskeyPleaseTryAgain": { "message": "Błędny passkey. Spróbuj ponownie." @@ -1099,7 +1141,7 @@ "message": "Utwórz konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nowy na Bitwarden?" }, "setAStrongPassword": { "message": "Ustaw silne hasło" @@ -1117,26 +1159,35 @@ "message": "Zaloguj się" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Zaloguj do Bitwarden" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Limit czasu uwierzytelniania" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Upłynął limit czasu uwierzytelniania. Uruchom ponownie proces logowania." }, "verifyIdentity": { "message": "Zweryfikuj swoją tożsamość" }, + "weDontRecognizeThisDevice": { + "message": "Nie rozpoznajemy tego urządzenia. Wpisz kod wysłany na Twój e-mail, aby zweryfikować tożsamość." + }, + "continueLoggingIn": { + "message": "Kontynuuj logowanie" + }, "whatIsADevice": { - "message": "What is a device?" + "message": "Czym jest urządzenie?" }, "aDeviceIs": { - "message": "A device is a unique installation of the Bitwarden app where you have logged in. Reinstalling, clearing app data, or clearing your cookies could result in a device appearing multiple times." + "message": "Urządzenie to unikalna instalacja aplikacji Bitwarden, w której się zalogowano. Ponowna instalacja, wyczyszczenie danych aplikacji lub usunięcie plików cookie może spowodować, że urządzenie pojawi się wielokrotnie." }, "logInInitiated": { "message": "Logowanie rozpoczęte" }, + "logInRequestSent": { + "message": "Żądanie wysłane" + }, "submit": { "message": "Wyślij" }, @@ -1260,10 +1311,10 @@ "message": "Adres e-mail" }, "yourVaultIsLockedV2": { - "message": "Twój sejf jest zablokowany." + "message": "Twój sejf jest zablokowany" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Twoje konto jest zablokowane" }, "uuid": { "message": "UUID" @@ -1300,7 +1351,7 @@ "message": "Nie masz uprawnień do przeglądania wszystkich elementów w tej kolekcji." }, "youDoNotHavePermissions": { - "message": "You do not have permissions to this collection" + "message": "Nie masz uprawnień do tej kolekcji" }, "noCollectionsInList": { "message": "Brak kolekcji do wyświetlenia." @@ -1326,11 +1377,38 @@ "notificationSentDevice": { "message": "Powiadomienie zostało wysłane na urządzenie." }, - "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "notificationSentDevicePart1": { + "message": "Odblokuj Bitwarden na swoim urządzeniu lub w" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "areYouTryingToAccessYourAccount": { + "message": "Czy próbujesz uzyskać dostęp do swojego konta?" + }, + "accessAttemptBy": { + "message": "Próba dostępu przez $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Potwierdź dostęp" + }, + "denyAccess": { + "message": "Odmów dostępu" + }, + "notificationSentDeviceAnchor": { + "message": "aplikacji internetowej" + }, + "notificationSentDevicePart2": { + "message": "Upewnij się, że fraza odcisku palca zgadza się z tą poniżej, zanim zatwierdzisz." + }, + "notificationSentDeviceComplete": { + "message": "Odblokuj Bitwarden na swoim urządzeniu. Przed zatwierdzeniem upewnij się, że fraza odcisku palca pasuje do tej poniżej." + }, + "aNotificationWasSentToYourDevice": { + "message": "Powiadomienie zostało wysłane na twoje urządzenie" }, "versionNumber": { "message": "Wersja $VERSION_NUMBER$", @@ -1423,7 +1501,7 @@ "message": "Klucz bezpieczeństwa FIDO U2F" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "Klucz dostępu" }, "webAuthnDesc": { "message": "Użyj dowolnego klucza bezpieczeństwa WebAuthn, aby uzyskać dostęp do swojego konta." @@ -1616,12 +1694,9 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Unikaj niejednoznacznych znaków", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Wygeneruj ponownie hasło" - }, "length": { "message": "Długość" }, @@ -1664,25 +1739,25 @@ "message": "Historia hasła" }, "generatorHistory": { - "message": "Generator history" + "message": "Historia generatora" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Wyczyść historię generatora" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Jeśli zatwierdzisz, wszystkie wygenerowane hasła zostaną usunięte z historii generatora. Czy chcesz kontynuować mimo to?" }, "noPasswordsInList": { "message": "Brak haseł." }, "clearHistory": { - "message": "Clear history" + "message": "Wyczyść historię" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Brak zawartości do pokazania" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Nic nie zostało wygenerowane przez ciebie w ostatnim czasie" }, "clear": { "message": "Wyczyść", @@ -1722,10 +1797,10 @@ "message": "Zaloguj się ponownie." }, "currentSession": { - "message": "Current session" + "message": "Aktualna sesja" }, "requestPending": { - "message": "Request pending" + "message": "Zapytanie oczekuje" }, "logBackInOthersToo": { "message": "Zaloguj się ponownie. Jeśli używasz innych aplikacji Bitwarden, wyloguj się i zaloguj ponownie również w nich." @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Niebezpieczna strefa" }, - "dangerZoneDesc": { - "message": "Uwaga - te operacje są nieodwracalne!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Zakończ sesje" }, @@ -1809,11 +1878,32 @@ "deauthorizeSessionsWarning": { "message": "Ta czynność spowoduje wylogowanie z bieżącej sesji, przez co konieczne będzie ponowne zalogowanie się. Zostaniesz również poproszony o ponowne logowanie dwustopniowe, jeśli masz włączoną tę opcję. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie godzinę." }, + "newDeviceLoginProtection": { + "message": "Logowanie nowego urządzenia" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Wyłącz ochronę logowania na nowym urządzeniu" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Włącz ochronę logowania na nowym urządzeniu" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Przejdź poniżej, aby wyłączyć e-maile weryfikacyjne wysyłane przez Bitwarden podczas logowania z nowego urządzenia." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Przejdź poniżej, aby Bitwarden wysyłał Ci e-maile weryfikacyjne podczas logowania z nowego urządzenia." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Po wyłączeniu ochrony logowania na nowym urządzeniu, każdy, kto zna Twoje hasło główne, może uzyskać dostęp do Twojego konta z dowolnego urządzenia. Aby chronić swoje konto bez e-maili weryfikacyjnych, skonfiguruj dwuetapowe logowanie." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Zapisano nowe zmiany ochrony logowania" + }, "sessionsDeauthorized": { "message": "Wszystkie sesje zostały zakończone" }, "accountIsOwnedMessage": { - "message": "This account is owned by $ORGANIZATIONNAME$", + "message": "To konto jest własnością $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -1878,7 +1968,7 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsLoginLink": { - "message": "new login", + "message": "Nowe dane logowania", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new login instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { @@ -2110,8 +2200,20 @@ "manage": { "message": "Zarządzaj" }, - "canManage": { - "message": "Może zarządzać" + "manageCollection": { + "message": "Zarządzaj kolekcją" + }, + "viewItems": { + "message": "Zobacz elementy" + }, + "viewItemsHidePass": { + "message": "Zobacz elementy, ukryte hasła" + }, + "editItems": { + "message": "Edytuj elementy" + }, + "editItemsHidePass": { + "message": "Edytuj elementy, ukryte hasła" }, "disable": { "message": "Wyłącz" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Wystąpił problem z odczytem klucza bezpieczeństwa. Spróbuj ponownie." }, - "twoFactorWebAuthnWarning": { - "message": "Z powodu ograniczeń platformy, klucze WebAuthn nie mogą być używane we wszystkich aplikacjach Bitwarden. Musisz włączyć inną metodę logowania dwustopniowego, aby zachować dostęp do konta w pozostałych sytuacjach. Wspierane platformy:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Sejf internetowy i rozszerzenia przeglądarki na komputerze/laptopie z przeglądarką obsługującą WebAuthn (Chrome, Opera, Vivaldi lub Firefox z włączoną obsługą FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Z powodu ograniczeń platformy, klucze WebAuthn nie mogą być używane we wszystkich aplikacjach Bitwarden. Musisz włączyć inną metodę logowania dwustopniowego, aby zachować dostęp do konta w pozostałych sytuacjach." }, "twoFactorRecoveryYourCode": { "message": "Kod odzyskiwania konta Bitwarden" @@ -2417,7 +2516,7 @@ "message": "Sprawdź ujawnione hasła" }, "timesExposed": { - "message": "Times exposed" + "message": "Ujawniono" }, "exposedXTimes": { "message": "Ujawnione $COUNT$ raz(y)", @@ -2454,7 +2553,7 @@ "message": "Brak elementów zawierających słabe hasła." }, "weakness": { - "message": "Weakness" + "message": "Słabe" }, "reusedPasswordsReport": { "message": "Identyczne hasła" @@ -2482,7 +2581,7 @@ "message": "Nie znaleźliśmy identycznych haseł w sejfie." }, "timesReused": { - "message": "Times reused" + "message": "Ponownie użyto" }, "reusedXTimes": { "message": "Wykorzystane $COUNT$ razy", @@ -2782,7 +2881,7 @@ "message": "Pobierz licencję" }, "viewBillingToken": { - "message": "View Billing Token" + "message": "Pokaż token płatności" }, "updateLicense": { "message": "Zaktualizuj licencję" @@ -2831,10 +2930,10 @@ "message": "Faktury" }, "noUnpaidInvoices": { - "message": "No unpaid invoices." + "message": "Brak nieopłaconych faktur." }, "noPaidInvoices": { - "message": "No paid invoices." + "message": "Brak opłaconych faktur." }, "paid": { "message": "Zapłacono", @@ -3279,7 +3378,10 @@ } }, "inviteSingleEmailDesc": { - "message": "You have 1 invite remaining." + "message": "Pozostało Ci 1 zaproszenie." + }, + "inviteZeroEmailDesc": { + "message": "Pozostało Ci 0 zaproszeń." }, "userUsingTwoStep": { "message": "Ten użytkownik korzysta z logowania dwustopniowego, aby chronić swoje konto." @@ -3315,7 +3417,7 @@ "message": "Użytkownik" }, "userDesc": { - "message": "Standardowy użytkownik, posiadający dostęp do kolekcji w Twojej organizacji." + "message": "Standardowy użytkownik, posiadający dostęp do kolekcji w Twojej organizacji" }, "all": { "message": "Wszyscy" @@ -3445,7 +3547,7 @@ } }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Zobacz wszystkie sposoby logowania" }, "viewAllLoginOptions": { "message": "Zobacz wszystkie sposoby logowania" @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Odłączone SSO" + }, "unlinkedSsoUser": { "message": "Odłącz logowanie jednokrotne SSO dla użytkownika %$ID$.", "placeholders": { @@ -3781,13 +3886,74 @@ "message": "Urządzenie" }, "loginStatus": { - "message": "Login status" + "message": "Status zalogowania" }, "firstLogin": { - "message": "First login" + "message": "Pierwsze logowanie" }, "trusted": { - "message": "Trusted" + "message": "Zaufane" + }, + "needsApproval": { + "message": "Wymaga zatwierdzenia" + }, + "areYouTryingtoLogin": { + "message": "Próbujesz się zalogować?" + }, + "logInAttemptBy": { + "message": "Próba logowania przez $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Typ urządenia" + }, + "ipAddress": { + "message": "Adres IP" + }, + "confirmLogIn": { + "message": "Potwierdź logowanie" + }, + "denyLogIn": { + "message": "Odrzuć logowanie" + }, + "thisRequestIsNoLongerValid": { + "message": "To żądanie jest już nieważne." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Logowanie potwierdzone dla $EMAIL$ na $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Odrzucono próby logowania z innego urządzenia. Jeśli to naprawdę Ty, spróbuj ponownie zalogować się za pomocą urządzenia." + }, + "loginRequestHasAlreadyExpired": { + "message": "Prośba logowania wygasła." + }, + "justNow": { + "message": "Teraz" + }, + "requestedXMinutesAgo": { + "message": "Poproszono $MINUTES$ minut temu", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } }, "creatingAccountOn": { "message": "Tworzenie konta na" @@ -3907,13 +4073,13 @@ "message": "Aktualizuj przeglądarkę" }, "generatingRiskInsights": { - "message": "Generating your risk insights..." + "message": "Generowanie informacji o ryzyku..." }, "updateBrowserDesc": { "message": "Używasz nieobsługiwanej przeglądarki. Sejf internetowy może działać niewłaściwie." }, "freeTrialEndPromptCount": { - "message": "Your free trial ends in $COUNT$ days.", + "message": "Twój okres próbny kończy się za $COUNT$ dni.", "placeholders": { "count": { "content": "$1", @@ -3922,7 +4088,7 @@ } }, "freeTrialEndPromptMultipleDays": { - "message": "$ORGANIZATION$, your free trial ends in $COUNT$ days.", + "message": "$ORGANIZATION$, twoja darmowa wersja próbna kończy się za $COUNT$ dni.", "placeholders": { "count": { "content": "$2", @@ -3935,7 +4101,7 @@ } }, "freeTrialEndPromptTomorrow": { - "message": "$ORGANIZATION$, your free trial ends tomorrow.", + "message": "$ORGANIZATION$, twoja darmowa wersja próbna kończy się jutro", "placeholders": { "organization": { "content": "$1", @@ -3944,10 +4110,10 @@ } }, "freeTrialEndPromptTomorrowNoOrgName": { - "message": "Your free trial ends tomorrow." + "message": "Twój okres próbny kończy się jutro." }, "freeTrialEndPromptToday": { - "message": "$ORGANIZATION$, your free trial ends today.", + "message": "$ORGANIZATION$, twoja darmowa wersja próbna kończy się dzisiaj", "placeholders": { "organization": { "content": "$1", @@ -3956,10 +4122,10 @@ } }, "freeTrialEndingTodayWithoutOrgName": { - "message": "Your free trial ends today." + "message": "Twój okres próbny kończy się dzisiaj." }, "clickHereToAddPaymentMethod": { - "message": "Click here to add a payment method." + "message": "Kliknij tutaj, aby dodać metodę płatności." }, "joinOrganization": { "message": "Dołącz do organizacji" @@ -4516,7 +4682,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." }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Zostaniesz powiadomiony po zatwierdzeniu prośby" }, "free": { "message": "Darmowy", @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Ustaw minimalne wymagania dla generatora hasła." }, - "passwordGeneratorPolicyInEffect": { - "message": "Co najmniej jedna zasada organizacji wpływa na ustawienia generatora." - }, "masterPasswordPolicyInEffect": { "message": "Co najmniej jedna zasada organizacji wymaga, aby hasło główne spełniało następujące wymagania:" }, @@ -4749,10 +4912,10 @@ "message": "Zaloguj się za pomocą logowania jednokrotnego SSO swojej organizacji. Aby rozpocząć, wpisz swój identyfikator organizacji." }, "singleSignOnEnterOrgIdentifier": { - "message": "Enter your organization's SSO identifier to begin" + "message": "Wprowadź identyfikator SSO swojej organizacji, aby rozpocząć" }, "singleSignOnEnterOrgIdentifierText": { - "message": "To log in with your SSO provider, enter your organization's SSO identifier to begin. You may need to enter this SSO identifier when you log in from a new device." + "message": "Aby zalogować się za pomocą dostawcy SSO, wprowadź identyfikator SSO organizacji, aby rozpocząć. Może być konieczne wprowadzenie identyfikatora SSO podczas logowania z nowego urządzenia." }, "enterpriseSingleSignOn": { "message": "Logowanie jednokrotne" @@ -4822,7 +4985,7 @@ "message": "Zablokuj użytkownikom możliwość dołączania do innych organizacji." }, "singleOrgPolicyDesc": { - "message": "Restrict members from joining other organizations. This policy is required for organizations that have enabled domain verification." + "message": "Ogranicz członków do dołączania do innych organizacji. Ta polityka jest wymagana dla organizacji, które włączyły weryfikację domeny." }, "singleOrgBlockCreateMessage": { "message": "Twoja obecna organizacja posiada zasady, które nie pozwalają na dołączanie do więcej niż jednej organizacji. Skontaktuj się z administratorami swojej organizacji lub zarejestruj się z innego konta Bitwarden." @@ -4831,7 +4994,7 @@ "message": "Członkowie organizacji, którzy nie są właścicielami lub administratorami i są już członkami innej organizacji zostaną usunięci z Twojej organizacji." }, "singleOrgPolicyMemberWarning": { - "message": "Non-compliant members will be placed in revoked status until they leave all other organizations. Administrators are exempt and can restore members once compliance is met." + "message": "Niezgodni członkowie zostaną objęci cofniętym statusem do czasu opuszczenia przez nich wszystkich innych organizacji. Administratorzy są zwolnieni i mogą przywrócić członków po osiągnięciu zgodności." }, "requireSso": { "message": "Uwierzytelnianie logowaniem jednokrotnym" @@ -5211,7 +5374,7 @@ "message": "Niestandardowe" }, "customDesc": { - "message": "Umożliwia zaawansowaną kontrolę uprawnień użytkownika." + "message": "Umożliwia zaawansowaną kontrolę uprawnień użytkownika" }, "customDescNonEnterpriseStart": { "message": "Role niestandardowe to ", @@ -5557,7 +5720,7 @@ "message": "Hasło zostało zresetowane!" }, "resetPasswordEnrollmentWarning": { - "message": "Rejestracja zezwala administratorom organizacji na zmianę Twojego hasła głównego. Czy na pewno chcesz się zarejestrować?" + "message": "Rejestracja zezwala administratorom organizacji na zmianę Twojego hasła głównego" }, "accountRecoveryPolicy": { "message": "Administracja odzyskiwaniem konta" @@ -5656,13 +5819,13 @@ "message": "Przywrócono dostęp do organizacji" }, "bulkFilteredMessage": { - "message": "Wykluczono, nie dotyczy tej akcji." + "message": "Wykluczono, nie dotyczy tej akcji" }, "nonCompliantMembersTitle": { - "message": "Non-compliant members" + "message": "Niezgodni członkowie" }, "nonCompliantMembersError": { - "message": "Members that are non-compliant with the Single organization or Two-step login policy cannot be restored until they adhere to the policy requirements" + "message": "Niezgodni członkowie z jedną organizacją lub dwustopniową regułą logowania nie mogą zostać przywróceni, dopóki nie będą przestrzegali wymogów polityki" }, "fingerprint": { "message": "Unikalny identyfikator konta" @@ -5680,17 +5843,17 @@ "message": "Błąd" }, "decryptionError": { - "message": "Decryption error" + "message": "Błąd odszyfrowywania" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden nie mógł odszyfrować elementów sejfu wymienionych poniżej." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Skontaktuj się z działem obsługi klienta,", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "aby uniknąć dalszej utraty danych.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -5709,7 +5872,7 @@ "message": "Nazwa dostawcy" }, "providerSetup": { - "message": "Dostawca został skonfigurowany." + "message": "Dostawca został skonfigurowany" }, "clients": { "message": "Klienci" @@ -6264,11 +6427,11 @@ "message": "Key Connector" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Połącz logowanie za pomocą SSO z Twoim serwerem kluczy odszyfrowania. Używając tej opcji, członkowie nie będą musieli używać swoich haseł głównych, aby odszyfrować dane sejfu.", + "message": "Połącz logowanie za pomocą SSO z Twoim serwerem kluczy odszyfrowania. Używając tej opcji, członkowie nie będą musieli używać swoich haseł głównych, aby odszyfrować dane sejfu", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { - "message": "Wymaga uwierzytelniania SSO i polityki jednej organizacji", + "message": "wymaga uwierzytelniania SSO i polityki jednej organizacji", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescEnd": { @@ -6330,7 +6493,7 @@ "message": "Pokaż token synchronizacji płatności" }, "generateBillingToken": { - "message": "Generate billing token" + "message": "Wygeneruj token płatności" }, "copyPasteBillingSync": { "message": "Skopiuj i wklej ten token do ustawień synchronizacji rozliczeniowej swojej organizacji." @@ -6339,7 +6502,7 @@ "message": "Twój token synchronizacji płatności może edytować ustawienia subskrypcji tej organizacji." }, "manageBillingTokenSync": { - "message": "Manage Billing Token" + "message": "Zarządzaj tokenami płatności" }, "setUpBillingSync": { "message": "Skonfiguruj synchronizację płatności" @@ -6405,7 +6568,7 @@ "message": "Token synchronizacji płatności" }, "automaticBillingSyncDesc": { - "message": "Automatic sync unlocks Families sponsorships and allows you to sync your license without uploading a file. After making updates in the Bitwarden cloud server, select Sync License to apply changes." + "message": "Automatyczna synchronizacja odblokowuje sponsorowanie rodzin i pozwala na synchronizację licencji bez przesyłania pliku. Po aktualizacjach w chmurze Bitwarden, wybierz licencję synchronizacji aby zastosować zmiany." }, "active": { "message": "Aktywny" @@ -6475,7 +6638,7 @@ "message": "Wymagane, jeśli identyfikatorem podmiotu nie jest adres URL." }, "offerNoLongerValid": { - "message": "This offer is no longer valid. Contact your organization administrators for more information." + "message": "Ta oferta nie jest już ważna. Aby uzyskać więcej informacji, skontaktuj się z administratorami organizacji." }, "openIdOptionalCustomizations": { "message": "Opcjonalne dostosowania" @@ -6554,23 +6717,14 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Co chcesz wygenerować?" - }, - "passwordType": { - "message": "Rodzaj hasła" - }, - "regenerateUsername": { - "message": "Wygeneruj ponownie nazwę użytkownika" - }, "generateUsername": { "message": "Wygeneruj nazwę użytkownika" }, "generateEmail": { - "message": "Generate email" + "message": "Wygeneruj e-mail" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Wartość musi być pomiędzy $MIN$ a $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -6584,7 +6738,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Użyj $RECOMMENDED$ znaków lub więcej, aby wygenerować silne hasło.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -6594,7 +6748,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Użyj $RECOMMENDED$ słów lub więcej, aby wygenerować silne hasło.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Rodzaj nazwy użytkownika" - }, "plusAddressedEmail": { "message": "Adres e-mail z plusem", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Użyj skonfigurowanej skrzynki catch-all w swojej domenie." }, + "useThisEmail": { + "message": "Użyj tego adresu e-mail" + }, "random": { "message": "Losowa", "description": "Generates domain-based username using random letters" @@ -6709,11 +6863,11 @@ "message": "Wygeneruj alias adresu e-mail z zewnętrznej usługi przekierowania." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Domena e-mail", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Wybierz domenę, która jest obsługiwana przez wybraną usługę", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -6822,9 +6976,6 @@ "message": "Nazwa hosta", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token dostępu API" - }, "deviceVerification": { "message": "Weryfikacja urządzenia" }, @@ -6858,7 +7009,7 @@ "description": "the text, 'SCIM', is an acronym and should not be translated." }, "scimIntegrationDescription": { - "message": "Automatically provision users and groups with your preferred identity provider via SCIM provisioning. Find supported integrations", + "message": "Automatycznie dostarczaj użytkownikom i grupom preferowanego dostawcy tożsamości za pomocą usługi SCIM. Znajdź obsługiwane integracje", "description": "the text, 'SCIM', is an acronym and should not be translated." }, "scimEnabledCheckboxDesc": { @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Brak kolekcji" }, - "canView": { - "message": "Może wyświetlać" - }, - "canViewExceptPass": { - "message": "Może wyświetlać z wyjątkiem haseł" - }, - "canEdit": { - "message": "Można edytować" - }, - "canEditExceptPass": { - "message": "Może edytować z wyjątkiem haseł" - }, "noCollectionsAdded": { "message": "Nie dodano kolekcji" }, @@ -7870,7 +8009,7 @@ "message": "Przesyłanie ręczne" }, "manualBillingTokenUploadDesc": { - "message": "If you do not want to opt into billing sync, manually upload your license here. This will not automatically unlock Families sponsorships." + "message": "Jeśli nie chcesz zrezygnować z synchronizacji rozliczeniowej, ręcznie prześlij swoją licencję tutaj. Nie odblokuje to automatycznie sponsorowania dla rodziny." }, "syncLicense": { "message": "Synchronizuj licencję" @@ -8139,16 +8278,16 @@ "message": "Logowanie rozpoczęte" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Zapamiętaj to urządzenie, aby przyszłe logowania były bezproblemowe" }, "deviceApprovalRequired": { "message": "Wymagane zatwierdzenie urządzenia. Wybierz opcję zatwierdzenia poniżej:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Wymagane zatwierdzenie urządzenia" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Wybierz opcję zatwierdzenia poniżej" }, "rememberThisDevice": { "message": "Zapamiętaj to urządzenie" @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Zaufane urządzenia" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Po uwierzytelnieniu członkowie odszyfrowają dane sejfu przy użyciu klucza zapisanego na ich urządzeniu.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Członkowie nie będą potrzebowali hasła głównego podczas logowania się za pomocą SSO. Hasło główne jest zastąpione kluczem szyfrowania przechowywanym na urządzeniu, co sprawia, że urządzenie jest zaufane. Pierwsze urządzenie, do którego użytkownik tworzy swoje konto i logi, będą zaufane. Nowe urządzenia będą musiały zostać zatwierdzone przez istniejące zaufane urządzenie lub przez administratora.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "polityka pojedynczej organizacji", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "pojedyncza organizacja", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": ",", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "polityka,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "polityka wymaganego SSO", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "Wymagane SSO", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "i", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "polityka i ", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "polityka administracji odzyskiwaniem", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "Administracja odzyskiwaniem konta", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "z automatycznym zapisem włączy się, gdy ta opcja jest używana.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "polityka włączy się, gdy ta opcja zostanie wykorzystana.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Uprawnienia w Twojej organizacji zostały zaktualizowane, musisz teraz ustawić hasło główne.", @@ -8275,16 +8414,16 @@ "message": "Zatwierdź prośbę" }, "deviceApproved": { - "message": "Device approved" + "message": "Urządzenie zostało zatwierdzone" }, "deviceRemoved": { - "message": "Device removed" + "message": "Urządzenie zostało usunięte" }, "removeDevice": { - "message": "Remove device" + "message": "Usuń urządzenie" }, "removeDeviceConfirmation": { - "message": "Are you sure you want to remove this device?" + "message": "Czy na pewno chcesz usunąć to urządzenie?" }, "noDeviceRequests": { "message": "Brak urządzeń do zatwierdzenia" @@ -8341,7 +8480,7 @@ "message": "Poproszono o zatwierdzenie urządzenia." }, "tdeOffboardingPasswordSet": { - "message": "User set a master password during TDE offboarding." + "message": "Użytkownik ustawił hasło główne podczas wyłączania TDE." }, "startYour7DayFreeTrialOfBitwardenFor": { "message": "Rozpocznij 7-dniowy darmowy okres próbny Bitwarden dla $ORG$", @@ -8392,7 +8531,7 @@ "message": "Brak adresu e-mail użytkownika" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Nie znaleziono aktywnego adresu e-mail. Trwa wylogowanie." }, "deviceTrusted": { "message": "Zaufano urządzeniu" @@ -8490,10 +8629,13 @@ "message": "Zarządzaj zachowaniami kolekcji w organizacji" }, "limitCollectionCreationDesc": { - "message": "Limit collection creation to owners and admins" + "message": "Ogranicz tworzenie kolekcji do właścicieli i administratorów" }, "limitCollectionDeletionDesc": { - "message": "Limit collection deletion to owners and admins" + "message": "Ogranicz usuwanie kolekcji do właścicieli i administratorów" + }, + "limitItemDeletionDesc": { + "message": "Ogranicz usuwanie elementów do członków z uprawnieniami do zarządzania" }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Właściciele i administratorzy mogą zarządzać wszystkimi zbiorami i elementami" @@ -8541,12 +8683,9 @@ "message": "Adres URL serwera" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "URL samodzielnie hostowanego serwera", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Domena aliasu" - }, "alreadyHaveAccount": { "message": "Masz już konto?" }, @@ -8608,10 +8747,10 @@ "readOnlyCollectionAccess": { "message": "Nie masz dostępu do zarządzania tą kolekcją." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Brak uprawnienia Zarządzaj Uprawnieniami" + "grantManageCollectionWarningTitle": { + "message": "Brak uprawnień do zarządzania kolekcją" }, - "grantAddAccessCollectionWarning": { + "grantManageCollectionWarning": { "message": "Przyznaj uprawnienie Zarządzaj Uprawnieniami w celu umożliwienia pełnego zarządzania kolekcją, w tym usuwania kolekcji." }, "grantCollectionAccess": { @@ -9068,47 +9207,47 @@ "message": "Użyj SDK Menedżera Sekretów Bitwarden w następujących językach programowania, aby zbudować własne aplikacje." }, "ssoDescStart": { - "message": "Configure", + "message": "Konfiguruj", "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, "ssoDescEnd": { - "message": "for Bitwarden using the implementation guide for your Identity Provider.", + "message": "dla Bitwarden przy użyciu instrukcji implementacyjnych dla Twojego dostawcy tożsamości.", "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, "userProvisioning": { - "message": "User provisioning" + "message": "Aprowizacja użytkowników" }, "scimIntegration": { "message": "SCIM" }, "scimIntegrationDescStart": { - "message": "Configure ", + "message": "Konfiguruj", "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, "scimIntegrationDescEnd": { - "message": "(System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider.", + "message": "(System do zarządzania tożsamością międzydomeną) w celu automatycznej aprowizacji użytkowników i grup do Bitwarden przy użyciu instrukcji implementacji dla Twojego dostawcy tożsamości.", "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, "bwdc": { "message": "Bitwarden Directory Connector" }, "bwdcDesc": { - "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." + "message": "Skonfiguruj Bitwarden Directory Connector do automatycznej aprowizacji użytkowników i grup za pomocą przewodnika implementacyjnego dla dostawcy tożsamości." }, "eventManagement": { - "message": "Event management" + "message": "Zarządzanie zdarzeniami" }, "eventManagementDesc": { - "message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform." + "message": "Zintegruj dzienniki zdarzeń Bitwarden ze swoim systemem SIEM (Security Information and Event Management), korzystając z poradnika implementacyjnego dla Twojej platformy." }, "deviceManagement": { - "message": "Device management" + "message": "Zarządzanie urządzeniem" }, "deviceManagementDesc": { - "message": "Configure device management for Bitwarden using the implementation guide for your platform." + "message": "Skonfiguruj zarządzanie urządzeniem Bitwarden za pomocą instrukcji implementacyjnych dla Twojej platformy." }, "integrationCardTooltip": { - "message": "Launch $INTEGRATION$ implementation guide.", + "message": "Uruchom przewodnik implementacyjny $INTEGRATION$.", "placeholders": { "integration": { "content": "$1", @@ -9117,7 +9256,7 @@ } }, "smIntegrationTooltip": { - "message": "Set up $INTEGRATION$.", + "message": "Skonfiguruj $INTEGRATION$.", "placeholders": { "integration": { "content": "$1", @@ -9126,7 +9265,7 @@ } }, "smSdkTooltip": { - "message": "View $SDK$ repository", + "message": "Zobacz repozytorium $SDK$", "placeholders": { "sdk": { "content": "$1", @@ -9135,7 +9274,7 @@ } }, "integrationCardAriaLabel": { - "message": "open $INTEGRATION$ implementation guide in a new tab.", + "message": "otwórz przewodnik implementacji $INTEGRATION$ w nowej karcie.", "placeholders": { "integration": { "content": "$1", @@ -9144,7 +9283,7 @@ } }, "smSdkAriaLabel": { - "message": "view $SDK$ repository in a new tab.", + "message": "wyświetl repozytorium $SDK$ w nowej karcie.", "placeholders": { "sdk": { "content": "$1", @@ -9153,7 +9292,7 @@ } }, "smIntegrationCardAriaLabel": { - "message": "set up $INTEGRATION$ implementation guide in a new tab.", + "message": "ustaw przewodnik implementacji $INTEGRATION$ w nowej karcie.", "placeholders": { "integration": { "content": "$1", @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "miesięcznie za członka" }, + "monthPerMemberBilledAnnually": { + "message": "miesięcznie na członka, rozliczanie rocznie" + }, "seats": { "message": "Miejsca" }, @@ -9210,13 +9352,13 @@ "message": "Kontynuuj konfigurację darmowego okresu próbnego Menedżera Sekretnych Bitwarden" }, "enterTeamsOrgInfo": { - "message": "Enter your Teams organization information" + "message": "Wprowadź informacje o organizacji Teams" }, "enterFamiliesOrgInfo": { "message": "Wprowadź informacje o swojej organizacji rodzinnej" }, "enterEnterpriseOrgInfo": { - "message": "Enter your Enterprise organization information" + "message": "Wprowadź informacje o organizacji Enterprise" }, "viewItemsIn": { "message": "Zobacz elementy w $NAME$", @@ -9271,10 +9413,10 @@ "message": "Dostawca usług zarządzanych" }, "managedServiceProvider": { - "message": "Managed service provider" + "message": "Zarządzany dostawca usług" }, "multiOrganizationEnterprise": { - "message": "Multi-organization enterprise" + "message": "Przedsiębiorstwo wielu organizacji" }, "orgSeats": { "message": "Miejsca w organizacji" @@ -9322,16 +9464,16 @@ "message": "Zaktualizowane informacje podatkowe" }, "billingInvalidTaxIdError": { - "message": "Invalid tax ID, if you believe this is an error please contact support." + "message": "Nieprawidłowy numer identyfikacji podatkowej, jeśli uważasz, że jest to błąd, skontaktuj się z pomocą techniczną." }, "billingTaxIdTypeInferenceError": { - "message": "We were unable to validate your tax ID, if you believe this is an error please contact support." + "message": "Nie mogliśmy zweryfikować Twojego identyfikatora podatkowego, jeśli uważasz, że jest to błąd, skontaktuj się z pomocą techniczną." }, "billingPreviewInvalidTaxIdError": { - "message": "Invalid tax ID, if you believe this is an error please contact support." + "message": "Nieprawidłowy numer identyfikacji podatkowej, jeśli uważasz, że jest to błąd, skontaktuj się z pomocą techniczną." }, "billingPreviewInvoiceError": { - "message": "An error occurred while previewing the invoice. Please try again later." + "message": "Wystąpił błąd podczas podglądu faktury. Spróbuj ponownie później." }, "unverified": { "message": "Niezweryfikowane" @@ -9598,13 +9740,13 @@ "message": "Dowiedz się więcej o API Bitwarden" }, "fileSends": { - "message": "File Sends" + "message": "Wysyłki plików" }, "textSends": { - "message": "Text Sends" + "message": "Wysyłki tekstów" }, "includesXMembers": { - "message": "for $COUNT$ member", + "message": "dla $COUNT$ członków", "placeholders": { "count": { "content": "$1", @@ -9691,28 +9833,37 @@ "message": "GB dodatkowej przestrzeni" }, "sshKeyAlgorithm": { - "message": "Key algorithm" + "message": "Algorytm klucza" + }, + "sshPrivateKey": { + "message": "Klucz prywatny" + }, + "sshPublicKey": { + "message": "Klucz publiczny" + }, + "sshFingerprint": { + "message": "Odcisk palca" }, "sshKeyFingerprint": { - "message": "Fingerprint" + "message": "Odcisk palca" }, "sshKeyPrivateKey": { - "message": "Private key" + "message": "Klucz prywatny" }, "sshKeyPublicKey": { - "message": "Public key" + "message": "Klucz publiczny" }, "sshKeyAlgorithmED25519": { "message": "ED25519" }, "sshKeyAlgorithmRSA2048": { - "message": "RSA 2048-Bit" + "message": "RSA 2048-bitowy" }, "sshKeyAlgorithmRSA3072": { - "message": "RSA 3072-Bit" + "message": "RSA 3072-bitowy" }, "sshKeyAlgorithmRSA4096": { - "message": "RSA 4096-Bit" + "message": "RSA 4096-bitowy" }, "premiumAccounts": { "message": "6 kont premium" @@ -9730,7 +9881,7 @@ "message": "Monitorowanie zdarzeń" }, "directoryIntegration": { - "message": "Directory integration" + "message": "Integracja katalogu" }, "passwordLessSso": { "message": "SSO bez hasła" @@ -9782,22 +9933,22 @@ "message": "Edytuj dostęp" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "Użyj pól tekstowych dla danych takich jak pytania bezpieczeństwa" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "Użyj ukrytych pól dla danych poufnych, takich jak hasło" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "Użyj pól wyboru, jeśli chcesz automatycznie wypełnić pole wyboru formularza, np. jak zapamiętywanie e-mail" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "Użyj powiązanego pola, gdy masz problemy z autouzupełnianiem na konkretnej stronie internetowej." }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "Wprowadź atrybut z: html id, name, aria-label lub placeholder." }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Uwzględnij wielkie litery", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -9838,23 +9989,23 @@ "message": "Czy na pewno chcesz trwale usunąć ten załącznik?" }, "manageSubscriptionFromThe": { - "message": "Manage subscription from the", + "message": "Zarządzaj subskrypcją z", "description": "This represents the beginning of a sentence. The full sentence will be 'Manage subscription from the Provider Portal', but 'Provider Portal' will be a link and thus cannot be included in the translation file." }, "toHostBitwardenOnYourOwnServer": { - "message": "To host Bitwarden on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up automatic sync in your self-hosted organization." + "message": "Aby hostować Bitwarden na własnym serwerze, musisz przesłać plik licencyjny. Aby wspierać darmowe plany rodzinne i zaawansowane możliwości rozliczeniowe dla Twojej własnej organizacji, musisz skonfigurować automatyczną synchronizację w swojej własnej organizacji." }, "selfHostingTitleProper": { - "message": "Self-Hosting" + "message": "Samodzielne hostowanie" }, "claim-domain-single-org-warning": { - "message": "Claiming a domain will turn on the single organization policy." + "message": "Rejestracja domeny włączy politykę jednej organizacji." }, "single-org-revoked-user-warning": { - "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." + "message": "Niezgodni członkowie zostaną odwołani. Administratorzy mogą przywrócić członków po opuszczeniu wszystkich innych organizacji." }, "deleteOrganizationUser": { - "message": "Delete $NAME$", + "message": "Usuń $NAME$", "placeholders": { "name": { "content": "$1", @@ -9864,7 +10015,7 @@ } }, "deleteOrganizationUserWarningDesc": { - "message": "This will permanently delete all items owned by $NAME$. Collection items are not impacted.", + "message": "Spowoduje to trwałe usunięcie wszystkich elementów należących do $NAME$. Nie ma to wpływu na elementy w kolekcji.", "description": "Warning description for the delete organization user dialog", "placeholders": { "name": { @@ -9874,11 +10025,11 @@ } }, "deleteManyOrganizationUsersWarningDesc": { - "message": "This will permanently delete all items owned by the following members. Collection items are not impacted.", + "message": "Spowoduje to trwałe usunięcie wszystkich elementów należących do następujących członków. Nie ma to wpływu na elementy w kolekcji.", "description": "Warning description for the bulk delete organization users dialog" }, "organizationUserDeleted": { - "message": "Deleted $NAME$", + "message": "Usunięto $NAME$", "placeholders": { "name": { "content": "$1", @@ -9887,10 +10038,10 @@ } }, "organizationUserDeletedDesc": { - "message": "The user was removed from the organization and all associated user data has been deleted." + "message": "Użytkownik został usunięty z organizacji i wszystkie powiązane dane z tym użytkownikiem zostały usunięte." }, "deletedUserId": { - "message": "Deleted user $ID$ - an owner / admin deleted the user account", + "message": "Usunięto użytkownika $ID$ - właściciel / administrator usunął te konto użytkownika", "placeholders": { "id": { "content": "$1", @@ -9899,7 +10050,7 @@ } }, "userLeftOrganization": { - "message": "User $ID$ left organization", + "message": "Użytkownik $ID$ opuścił organizację", "placeholders": { "id": { "content": "$1", @@ -9908,7 +10059,7 @@ } }, "suspendedOrganizationTitle": { - "message": "The $ORGANIZATION$ is suspended", + "message": "$ORGANIZATION$ jest zawieszona", "placeholders": { "organization": { "content": "$1", @@ -9917,52 +10068,61 @@ } }, "suspendedUserOrgMessage": { - "message": "Contact your organization owner for assistance." + "message": "Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, "suspendedOwnerOrgMessage": { - "message": "To regain access to your organization, add a payment method." + "message": "Aby odzyskać dostęp do swojej organizacji, dodaj metodę płatności." }, "deleteMembers": { - "message": "Delete members" + "message": "Usuń członków" }, "noSelectedMembersApplicable": { - "message": "This action is not applicable to any of the selected members." + "message": "Ta akcja nie ma zastosowania do żadnego z wybranych członków." }, "deletedSuccessfully": { - "message": "Deleted successfully" + "message": "Usunięto pomyślnie" }, "freeFamiliesSponsorship": { - "message": "Remove Free Bitwarden Families sponsorship" + "message": "Usuń darmowe sponsorowanie rodzin Bitwarden" }, "freeFamiliesSponsorshipPolicyDesc": { - "message": "Do not allow members to redeem a Families plan through this organization." + "message": "Nie zezwalaj członkom na wykupienie planu rodzin za pośrednictwem tej organizacji." }, "verifyBankAccountWithStatementDescriptorWarning": { - "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the organization's billing page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + "message": "Płatność na rachunku bankowym jest dostępna tylko dla klientów w Stanach Zjednoczonych. Będziesz musiał zweryfikować swoje konto bankowe. Dokonamy mikrowpłaty w ciągu następnych 1-2 dni roboczych. Wprowadź kod deskryptora instrukcji z tej wpłaty na stronie rozliczeniowej organizacji, aby zweryfikować konto bankowe. Niezweryfikowanie konta bankowego spowoduje nieodebraną płatność, a subskrypcja zostanie zawieszona." }, "verifyBankAccountWithStatementDescriptorInstructions": { - "message": "We have made a micro-deposit to your bank account (this may take 1-2 business days). Enter the six-digit code starting with 'SM' found on the deposit description. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + "message": "Dokonaliśmy mikrowpłaty na Twoje konto bankowe (może to zająć 1-2 dni robocze). Wprowadź sześciocyfrowy kod zaczynający się od 'SM' znaleziony w opisie depozytu. Niezweryfikowanie konta bankowego spowoduje nieodebraną płatność, a subskrypcja zostanie zawieszona." }, "descriptorCode": { - "message": "Descriptor code" + "message": "Kod deskryptora" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Nie można usunąć kolekcji z uprawnieniami tylko do przeglądania: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } }, "importantNotice": { - "message": "Important notice" + "message": "Ważna informacja" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Skonfiguruj dwustopniowe logowanie" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden wyśle kod na Twój adres e-mail w celu zweryfikowania logowania z nowych urządzeń, począwszy od lutego 2025 r." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Możesz skonfigurować dwustopniowe logowanie jako alternatywny sposób ochrony konta lub zmienić swój adres e-mail, do którego masz dostęp." }, "remindMeLater": { - "message": "Remind me later" + "message": "Przypomnij mi później" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Czy masz pewny dostęp do swojego adresu e-mail, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -9971,49 +10131,49 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Nie, nie mam" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Tak, mam pewny dostęp do mojego adresu e-mail" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Włącz dwustopniowe logowanie" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Zmień adres e-mail konta" }, "removeMembers": { - "message": "Remove members" + "message": "Usuń użytkowników" }, "devices": { - "message": "Devices" + "message": "Urządzenia" }, "deviceListDescription": { - "message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now." + "message": "Twoje konto zostało zalogowane na każdym z poniższych urządzeń. Jeśli nie rozpoznajesz urządzenia, usuń je teraz." }, "deviceListDescriptionTemp": { - "message": "Your account was logged in to each of the devices below." + "message": "Twoje konto zostało zalogowane na każdym z poniższych urządzeń." }, "claimedDomains": { - "message": "Claimed domains" + "message": "Zarejestrowane domeny" }, "claimDomain": { - "message": "Claim domain" + "message": "Zarejestruj domenę" }, "reclaimDomain": { - "message": "Reclaim domain" + "message": "Wyrejestruj domenę" }, "claimDomainNameInputHint": { - "message": "Example: mydomain.com. Subdomains require separate entries to be claimed." + "message": "Przykład: mydomain.com. Subdomeny wymagają osobnych wpisów celem weryfikacji." }, "automaticClaimedDomains": { - "message": "Automatic Claimed Domains" + "message": "Automatycznie zarejestrowane domeny" }, "automaticDomainClaimProcess": { - "message": "Bitwarden will attempt to claim the domain 3 times during the first 72 hours. If the domain can’t be claimed, check the DNS record in your host and manually claim. The domain will be removed from your organization in 7 days if it is not claimed." + "message": "Bitwarden spróbuje zweryfikować domenę 3 razy w ciągu pierwszych 72 godzin. Jeśli nie będzie można zweryfikować domeny, sprawdź rekord DNS na swoim serwerze i wprowadź go ręcznie. Domena zostanie usunięta z Twojej organizacji w ciągu 7 dni, jeśli nie zostanie zweryfikowana" }, "domainNotClaimed": { - "message": "$DOMAIN$ not claimed. Check your DNS records.", + "message": "$DOMAIN$ nie została zweryfikowana. Sprawdź swój rekord DNS.", "placeholders": { "DOMAIN": { "content": "$1", @@ -10022,19 +10182,19 @@ } }, "domainStatusClaimed": { - "message": "Claimed" + "message": "Zweryfikowano" }, "domainStatusUnderVerification": { - "message": "Under verification" + "message": "Podczas weryfikacji" }, "claimedDomainsDesc": { - "message": "Claim a domain to own all member accounts whose email address matches the domain. Members will be able to skip the SSO identifier when logging in. Administrators will also be able to delete member accounts." + "message": "Zweryfikuj domenę do posiadania wszystkich kont użytkowników, których adres e-mail pasuje do domeny. Członkowie będą mogli pominąć identyfikator SSO podczas logowania. Administratorzy będą również mogli usuwać konta członków." }, "invalidDomainNameClaimMessage": { - "message": "Input is not a valid format. Format: mydomain.com. Subdomains require separate entries to be claimed." + "message": "Wprowadzone dane są niepoprawne. Format: mydomain.com. Subdomeny wymagają osobnych wpisów celem weryfikacji." }, "domainClaimedEvent": { - "message": "$DOMAIN$ claimed", + "message": "Zweryfikowano $DOMAIN$", "placeholders": { "DOMAIN": { "content": "$1", @@ -10043,7 +10203,7 @@ } }, "domainNotClaimedEvent": { - "message": "$DOMAIN$ not claimed", + "message": "$DOMAIN$ nie została zweryfikowana", "placeholders": { "DOMAIN": { "content": "$1", @@ -10052,7 +10212,7 @@ } }, "updatedRevokeSponsorshipConfirmationForSentSponsorship": { - "message": "If you remove $EMAIL$, the sponsorship for this Family plan cannot be redeemed. Are you sure you want to continue?", + "message": "Jeśli usuniesz $EMAIL$, sponsorowanie tego planu rodziny nie może zostać zrealizowane. Czy na pewno chcesz kontynuować?", "placeholders": { "email": { "content": "$1", @@ -10061,7 +10221,7 @@ } }, "updatedRevokeSponsorshipConfirmationForAcceptedSponsorship": { - "message": "If you remove $EMAIL$, the sponsorship for this Family plan will end and the saved payment method will be charged $40 + applicable tax on $DATE$. You will not be able to redeem a new sponsorship until $DATE$. Are you sure you want to continue?", + "message": "Jeśli usuniesz $EMAIL$, sponsorowanie tego planu rodzinnego zakończy się, a zapisana metoda płatności zostanie naliczona 40 USD + obowiązujący podatek od $DATE$. Nie będziesz w stanie wykupić nowego sponsora do dnia $DATE$. Czy na pewno chcesz kontynuować?", "placeholders": { "email": { "content": "$1", @@ -10074,13 +10234,13 @@ } }, "domainClaimed": { - "message": "Domain claimed" + "message": "Domena zarejestrowana" }, "organizationNameMaxLength": { - "message": "Organization name cannot exceed 50 characters." + "message": "Nazwa organizacji nie może przekraczać 50 znaków." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Twoja subskrypcja zostanie wkrótce odnowiona. Aby zapewnić nieprzerwaną usługę, skontaktuj się z $RESELLER$ , aby potwierdzić odnowienie przed $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "Faktura za subskrypcję została wystawiona dnia $ISSUED_DATE$. Aby zapewnić nieprzerwaną usługę, skontaktuj się z $RESELLER$ , aby potwierdzić odnowienie przed dniem $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "Faktura za subskrypcję nie została zapłacona. Aby zapewnić nieprzerwaną usługę, skontaktuj się z $RESELLER$ , aby potwierdzić odnowienie przed $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10123,18 +10283,67 @@ } }, "restartOrganizationSubscription": { - "message": "Organization subscription restarted" + "message": "Subskrypcja organizacji zrestartowana" }, "restartSubscription": { - "message": "Restart your subscription" + "message": "Zrestartuj swoją subskrypcję" }, "suspendedManagedOrgMessage": { - "message": "Contact $PROVIDER$ for assistance.", + "message": "Skontaktuj się z $PROVIDER$ w celu uzyskania pomocy.", "placeholders": { "provider": { "content": "$1", "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administratorzy mają teraz możliwość usunięcia kont członków, które należą do zarejestrowanej domeny." + }, + "deleteManagedUserWarningDesc": { + "message": "Ta akcja usunie konto członka, w tym wszystkie elementy w ich sejfie. To zastępuje poprzednią akcję Usunięcia." + }, + "deleteManagedUserWarning": { + "message": "Usuń jest nową akcją!" + }, + "seatsRemaining": { + "message": "Pozostało $REMAINING$ miejsc z $TOTAL$ miejsc przypisanych do tej organizacji. Skontaktuj się z dostawcą, aby zarządzać subskrypcją.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Istniejąca organizacja" + }, + "selectOrganizationProviderPortal": { + "message": "Wybierz organizację, aby dodać do portalu dostawcy." + }, + "noOrganizations": { + "message": "Nie ma organizacji, którą można by wyświetlić" + }, + "yourProviderSubscriptionCredit": { + "message": "Subskrypcja twojego dostawcy otrzyma kredyt za pozostały czas w subskrypcji organizacji." + }, + "doYouWantToAddThisOrg": { + "message": "Czy chcesz dodać tę organizację do $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Dodano istniejącą organizację" + }, + "assignedExceedsAvailable": { + "message": "Przypisane miejsca przekraczają dostępne miejsca." } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index b911d5b273aa..85efb0c10279 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Membros de risco" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total de membros" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Todos os aplicativos" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Que tipo de item é este?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Editar Pasta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Domínio de base", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Nome do item" }, - "cannotRemoveViewOnlyCollections": { - "message": "Você não pode remover coleções com permissões de Somente leitura: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verifique sua identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Login iniciado" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Enviar" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Uma notificação foi enviada para seu dispositivo." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Uma notificação foi enviada para seu dispositivo." }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Certifique-se que sua conta esteja desbloqueado e que a frase de impressão digital corresponda à do outro dispositivo." - }, "versionNumber": { "message": "Versão $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Evitar caracteres ambíguos", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Gerar Nova Senha" - }, "length": { "message": "Comprimento" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Zona de perigo" }, - "dangerZoneDesc": { - "message": "Cuidado, essas ações não são reversíveis!" - }, - "dangerZoneDescSingular": { - "message": "Cuidado, esta ação não é reversível!" - }, "deauthorizeSessions": { "message": "Desautorizar sessões" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "O processo também desconectará você da sua sessão atual, exigindo que você inicie a sessão novamente. Você também será solicitado a efetuar login em duas etapas novamente, se estiver ativado. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Todas as Sessões Desautorizadas" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Gerenciar" }, - "canManage": { - "message": "Pode gerenciar" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Desabilitar" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Houve um problema ao ler a chave de segurança. Tente novamente." }, - "twoFactorWebAuthnWarning": { - "message": "Devido às limitações da plataforma, o WebAuthn não pode ser usado em todos os aplicativos do Bitwarden. Você deve habilitar outro provedor de login em duas etapas, para que você possa acessar a sua conta quando o WebAuthn não puder ser usado. Plataformas suportadas:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Cofre web e extensões de navegador em um desktop/laptop com um navegador habilitado para WebAuthn (Chrome, Opera, Vivaldi ou Firefox com o FIDO U2F ativado)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Seu código de recuperação de login em duas etapas do Bitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Este usuário está usando o login em duas etapas para proteger a sua conta." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "SSO desvinculado para o usuário $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Criando conta em" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Defina os requisitos mínimos para configuração do gerador de senhas." }, - "passwordGeneratorPolicyInEffect": { - "message": "Uma ou mais políticas da organização estão afetando as suas configurações do gerador." - }, "masterPasswordPolicyInEffect": { "message": "Uma ou mais políticas da organização exigem que a sua senha mestra cumpra aos seguintes requisitos:" }, @@ -6554,15 +6717,6 @@ "message": "Gerador", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "O que você gostaria de gerar?" - }, - "passwordType": { - "message": "Tipo de Senha" - }, - "regenerateUsername": { - "message": "Recriar Usuário" - }, "generateUsername": { "message": "Gerar Usuário" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Tipo de Usuário" - }, "plusAddressedEmail": { "message": "E-mail alternativo (com um +)", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use o catch-all configurado no seu domínio." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Aleatório", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token de acesso API" - }, "deviceVerification": { "message": "Verificação do dispositivo" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Sem coleções" }, - "canView": { - "message": "Pode ver" - }, - "canViewExceptPass": { - "message": "Pode ver, exceto senhas" - }, - "canEdit": { - "message": "Pode editar" - }, - "canEditExceptPass": { - "message": "Pode editar, exceto senhas" - }, "noCollectionsAdded": { "message": "Nenhuma coleção adicionada" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Dispositivos confiáveis" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Uma vez autenticados, os membros descriptografarão os dados do cofre usando uma chave armazenada no seu dispositivo", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "organização única", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "política,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "Necessário SSO", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "política e", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "gerenciar recuperação de conta", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "política com inscrição automática será ativada quando esta opção for utilizada.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "As permissões da sua organização foram atualizadas, exigindo que você defina uma senha mestra.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limitar exclusão de coleção a proprietários e administradores" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Proprietários e administradores podem gerenciar todas as coleções e itens" }, @@ -8544,9 +8686,6 @@ "message": "URL do servidor auto-host", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias do domínio" - }, "alreadyHaveAccount": { "message": "Já tem uma conta?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Você não tem acesso para gerenciar esta coleção." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Falta conceder o Poder de Gerenciar Permissões" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Conceda o poder de gerenciar permissões para habilitar o gerenciamento completo da coleção, incluindo a exclusão da coleção." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Conceder acesso de grupos ou membros a esta coleção." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "mês por membro" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Lugares" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Algoritmo da chave" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Impressão digital" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Código do descritor" }, + "cannotRemoveViewOnlyCollections": { + "message": "Você não pode remover coleções com permissões de Somente leitura: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 637c65f644d8..964e45949586 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Membros em risco" }, + "atRiskMembersWithCount": { + "message": "Membros em risco ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Aplicações em risco ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Estes membros estão a iniciar sessão em aplicações com palavras-passe fracas, expostas ou reutilizadas." + }, + "atRiskApplicationsDescription": { + "message": "Estas aplicações têm palavras-passe fracas, expostas ou reutilizadas." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Estes membros estão a iniciar sessão no $APPNAME$ com palavras-passe fracas, expostas ou reutilizadas.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total de membros" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Todas de aplicações" }, + "unmarkAsCriticalApp": { + "message": "Desmarcar como aplicação crítica" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Aplicação crítica desmarcada com sucesso" + }, "whatTypeOfItem": { "message": "Que tipo de item é este?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Editar pasta" }, + "newFolder": { + "message": "Nova pasta" + }, + "folderName": { + "message": "Nome da pasta" + }, + "folderHintText": { + "message": "Crie uma subpasta adicionando o nome da pasta principal seguido de um \"/\". Exemplo: Redes Sociais/Fóruns" + }, + "deleteFolderPermanently": { + "message": "Tem a certeza de que pretende eliminar permanentemente esta pasta?" + }, "baseDomain": { "message": "Domínio de base", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Nome do item" }, - "cannotRemoveViewOnlyCollections": { - "message": "Não é possível remover coleções com permissões de Apenas visualização: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verifique a sua identidade" }, + "weDontRecognizeThisDevice": { + "message": "Não reconhecemos este dispositivo. Introduza o código enviado para o seu e-mail para verificar a sua identidade." + }, + "continueLoggingIn": { + "message": "Continuar a iniciar sessão" + }, "whatIsADevice": { "message": "O que é um dispositivo?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "A preparar o início de sessão" }, + "logInRequestSent": { + "message": "Pedido enviado" + }, "submit": { "message": "Submeter" }, @@ -1174,7 +1225,7 @@ "message": "Dica da palavra-passe mestra" }, "masterPassHintText": { - "message": "Se se esquecer da sua palavra-passe, a dica da palavra-passe pode ser enviada para o seu e-mail. Máximo de $CURRENT$/$MAXIMUM$ caracteres.", + "message": "Se se esquecer da sua palavra-passe, a dica da palavra-passe pode ser enviada para o seu e-mail. Máximo de $CURRENT$/$MAXIMUM$ carateres.", "placeholders": { "current": { "content": "$1", @@ -1223,7 +1274,7 @@ "message": "É necessário reescrever a palavra-passe mestra." }, "masterPasswordMinlength": { - "message": "A palavra-passe mestra deve ter pelo menos $VALUE$ caracteres.", + "message": "A palavra-passe mestra deve ter pelo menos $VALUE$ carateres.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Foi enviada uma notificação para o seu dispositivo." }, + "notificationSentDevicePart1": { + "message": "Desbloqueie o Bitwarden no seu dispositivo ou no " + }, + "areYouTryingToAccessYourAccount": { + "message": "Está a tentar aceder à sua conta?" + }, + "accessAttemptBy": { + "message": "Tentativa de acesso por $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirmar acesso" + }, + "denyAccess": { + "message": "Recusar acesso" + }, + "notificationSentDeviceAnchor": { + "message": "aplicação web" + }, + "notificationSentDevicePart2": { + "message": "Certifique-se de que a frase da impressão digital corresponde à frase abaixo indicada antes de a aprovar." + }, + "notificationSentDeviceComplete": { + "message": "Desbloqueie o Bitwarden no seu dispositivo. Certifique-se de que a frase da impressão digital corresponde à frase abaixo antes de a aprovar." + }, "aNotificationWasSentToYourDevice": { "message": "Foi enviada uma notificação para o seu dispositivo" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Certifique-se de que a sua conta está desbloqueada e que a frase de impressão digital corresponde à do outro dispositivo" - }, "versionNumber": { "message": "Versão $VERSION_NUMBER$", "placeholders": { @@ -1608,20 +1686,17 @@ "message": "Mínimo de números" }, "minSpecial": { - "message": "Mínimo de caracteres especiais", + "message": "Mínimo de carateres especiais", "description": "Minimum special characters" }, "ambiguous": { - "message": "Evitar caracteres ambíguos", + "message": "Evitar carateres ambíguos", "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Evitar caracteres ambíguos", + "message": "Evitar carateres ambíguos", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerar palavra-passe" - }, "length": { "message": "Comprimento" }, @@ -1641,7 +1716,7 @@ "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caracteres especiais (!@#$%^&*)" + "message": "Carateres especiais (!@#$%^&*)" }, "numWords": { "message": "Número de palavras" @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Zona de risco" }, - "dangerZoneDesc": { - "message": "Cuidado, estas ações são irreversíveis!" - }, - "dangerZoneDescSingular": { - "message": "Cuidado, esta ação não é reversível!" - }, "deauthorizeSessions": { "message": "Desautorizar sessões" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Ao prosseguir, também terminará a sua sessão atual e terá de iniciar sessão novamente. Ser-lhe-á também pedido que volte efetuar a verificação de dois passos, se estiver configurada. As sessões ativas noutros dispositivos podem continuar ativas até uma hora." }, + "newDeviceLoginProtection": { + "message": "Início de sessão de um novo dispositivo" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Desativar a proteção de início de sessão de novos dispositivos" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Ativar a proteção de início de sessão de novos dispositivos" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceda da seguinte forma para desativar os e-mails de verificação que o Bitwarden envia quando inicia sessão a partir de um novo dispositivo." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceda da seguinte forma para que o Bitwarden lhe envie e-mails de verificação quando iniciar sessão a partir de um novo dispositivo." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Com a proteção de início de sessão de novos dispositivos desativada, qualquer pessoa com a sua palavra-passe mestra pode aceder à sua conta a partir de qualquer dispositivo. Para proteger a sua conta sem e-mails de verificação, configure a verificação de dois passos." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Alterações na proteção do início de sessão do novo dispositivo guardadas" + }, "sessionsDeauthorized": { "message": "Todas as sessões desautorizadas" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Gerir" }, - "canManage": { - "message": "Pode gerir" + "manageCollection": { + "message": "Gerir coleção" + }, + "viewItems": { + "message": "Ver itens" + }, + "viewItemsHidePass": { + "message": "Ver itens, palavras-passe ocultas" + }, + "editItems": { + "message": "Editar itens" + }, + "editItemsHidePass": { + "message": "Editar itens, palavras-passe ocultas" }, "disable": { "message": "Desativar" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Ocorreu um problema ao ler a chave de segurança. Tente novamente." }, - "twoFactorWebAuthnWarning": { - "message": "Devido a limitações da plataforma, o WebAuthn não pode ser utilizado em todas as aplicações Bitwarden. Deve configurar outro fornecedor de verificação de dois passos para que possa aceder à sua conta quando o WebAuthn não puder ser utilizado. Plataformas suportadas:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Cofre web e extensões do navegador num computador de secretária/portátil com um navegador compatível com WebAuthn (Chrome, Opera, Vivaldi ou Firefox com FIDO U2F ativado)." + "twoFactorWebAuthnWarning1": { + "message": "Devido a limitações da plataforma, o WebAuthn não pode ser utilizado em todas as aplicações Bitwarden. Deve configurar outro fornecedor de verificação de dois passos para poder aceder à sua conta quando o WebAuthn não puder ser utilizado." }, "twoFactorRecoveryYourCode": { "message": "O seu código Bitwarden de recuperação da verificação de dois passos" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Ainda tem 1 convite." }, + "inviteZeroEmailDesc": { + "message": "Restam-lhe 0 convites." + }, "userUsingTwoStep": { "message": "Este utilizador está a utilizar a verificação de dois passos para proteger a sua conta." }, @@ -3732,8 +3834,11 @@ } } }, + "unlinkedSso": { + "message": "SSO desvinculado." + }, "unlinkedSsoUser": { - "message": "SSO do utilizador $ID$ desligado.", + "message": "SSO desvinculado para o utilizador $ID$.", "placeholders": { "id": { "content": "$1", @@ -3789,6 +3894,67 @@ "trusted": { "message": "Confiável" }, + "needsApproval": { + "message": "Precisa de aprovação" + }, + "areYouTryingtoLogin": { + "message": "Está a tentar iniciar sessão?" + }, + "logInAttemptBy": { + "message": "Tentativa de início de sessão por $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Tipo de dispositivo" + }, + "ipAddress": { + "message": "Endereço IP" + }, + "confirmLogIn": { + "message": "Confirmar início de sessão" + }, + "denyLogIn": { + "message": "Recusar início de sessão" + }, + "thisRequestIsNoLongerValid": { + "message": "Este pedido já não é válido." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Início de sessão confirmado para $EMAIL$ no $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Recusou uma tentativa de início de sessão de outro dispositivo. Se foi realmente o caso, tente iniciar sessão com o dispositivo novamente." + }, + "loginRequestHasAlreadyExpired": { + "message": "O pedido de início de sessão já expirou." + }, + "justNow": { + "message": "Agora mesmo" + }, + "requestedXMinutesAgo": { + "message": "Pedido há $MINUTES$ minutos", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "A criar conta em" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Definir requisitos para o gerador de palavras-passe." }, - "passwordGeneratorPolicyInEffect": { - "message": "Uma ou mais políticas da organização estão a afetar as suas definições do gerador." - }, "masterPasswordPolicyInEffect": { "message": "Uma ou mais políticas da organização exigem que a sua palavra-passe mestra cumpra os seguintes requisitos:" }, @@ -4608,16 +4771,16 @@ } }, "policyInEffectUppercase": { - "message": "Contém um ou mais caracteres em maiúsculas" + "message": "Contém um ou mais carateres em maiúsculas" }, "policyInEffectLowercase": { - "message": "Contém um ou mais caracteres em minúsculas" + "message": "Contém um ou mais carateres em minúsculas" }, "policyInEffectNumbers": { "message": "Contém um ou mais números" }, "policyInEffectSpecial": { - "message": "Contém um ou mais dos seguintes caracteres especiais $CHARS$", + "message": "Contém um ou mais dos seguintes carateres especiais $CHARS$", "placeholders": { "chars": { "content": "$1", @@ -4807,13 +4970,13 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" }, "unlinkSso": { - "message": "Desativar SSO" + "message": "Desvincular SSO" }, "unlinkSsoConfirmation": { - "message": "Tem a certeza de que pretende desligar o SSO desta organização?" + "message": "Tem a certeza de que pretende desvincular o SSO desta organização?" }, "linkSso": { - "message": "Ativar SSO" + "message": "Vincular SSO" }, "singleOrg": { "message": "Organização única" @@ -6450,7 +6613,7 @@ "message": "obrigatório" }, "charactersCurrentAndMaximum": { - "message": "$CURRENT$/$MAX$ máximo de caracteres", + "message": "$CURRENT$/$MAX$ máximo de carateres", "placeholders": { "current": { "content": "$1", @@ -6463,7 +6626,7 @@ } }, "characterMaximum": { - "message": "Máximo de $MAX$ caracteres", + "message": "Máximo de $MAX$ carateres", "placeholders": { "max": { "content": "$1", @@ -6554,15 +6717,6 @@ "message": "Gerador", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "O que é que gostaria de gerar?" - }, - "passwordType": { - "message": "Tipo de palavra-passe" - }, - "regenerateUsername": { - "message": "Regenerar nome de utilizador" - }, "generateUsername": { "message": "Gerar nome de utilizador" }, @@ -6584,7 +6738,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Utilize $RECOMMENDED$ caracteres ou mais para gerar uma palavra-passe forte.", + "message": " Utilize $RECOMMENDED$ carateres ou mais para gerar uma palavra-passe forte.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Tipo de nome de utilizador" - }, "plusAddressedEmail": { "message": "E-mail com subendereço", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Utilize a caixa de entrada de captura geral configurada para o seu domínio." }, + "useThisEmail": { + "message": "Utilizar este e-mail" + }, "random": { "message": "Aleatório", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Nome de domínio", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token de acesso à API" - }, "deviceVerification": { "message": "Verificação do dispositivo" }, @@ -6914,7 +7065,7 @@ "message": "O campo não é um endereço de e-mail." }, "inputMinLength": { - "message": "O campo deve ter pelo menos $COUNT$ caracteres.", + "message": "O campo deve ter pelo menos $COUNT$ carateres.", "placeholders": { "count": { "content": "$1", @@ -6923,7 +7074,7 @@ } }, "inputMaxLength": { - "message": "O campo não pode exceder os $COUNT$ caracteres de comprimento.", + "message": "O campo não pode exceder os $COUNT$ carateres de comprimento.", "placeholders": { "count": { "content": "$1", @@ -6932,7 +7083,7 @@ } }, "inputForbiddenCharacters": { - "message": "Não são permitidos os seguintes caracteres: $CHARACTERS$", + "message": "Não são permitidos os seguintes carateres: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -6941,7 +7092,7 @@ } }, "inputMinValue": { - "message": "O valor do campo tem de ser, pelo menos, $MIN$ caracteres.", + "message": "O valor do campo tem de ser, pelo menos, $MIN$ carateres.", "placeholders": { "min": { "content": "$1", @@ -6950,7 +7101,7 @@ } }, "inputMaxValue": { - "message": "O valor do campo não pode exceder os $MAX$ caracteres.", + "message": "O valor do campo não pode exceder os $MAX$ carateres.", "placeholders": { "max": { "content": "$1", @@ -7082,11 +7233,11 @@ "message": "Limpar tudo" }, "toggleCharacterCount": { - "message": "Mostrar/ocultar contagem de caracteres", + "message": "Mostrar/ocultar contagem de carateres", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, "passwordCharacterCount": { - "message": "Contagem de caracteres da palavra-passe", + "message": "Contagem de carateres da palavra-passe", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, "hide": { @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Sem coleções" }, - "canView": { - "message": "Pode ver" - }, - "canViewExceptPass": { - "message": "Pode ver, excepto palavras-passe" - }, - "canEdit": { - "message": "Pode editar" - }, - "canEditExceptPass": { - "message": "Pode editar, excepto palavras-passe" - }, "noCollectionsAdded": { "message": "Não foram adicionadas coleções" }, @@ -8090,7 +8229,7 @@ "message": "Palavra-passe fraca identificada e encontrada numa violação de dados. Utilize uma palavra-passe forte e única para proteger a sua conta. Tem a certeza de que pretende utilizar esta palavra-passe?" }, "characterMinimum": { - "message": "$LENGTH$ caracteres no mínimo", + "message": "$LENGTH$ carateres no mínimo", "placeholders": { "length": { "content": "$1", @@ -8099,7 +8238,7 @@ } }, "masterPasswordMinimumlength": { - "message": "A palavra-passe mestra deve ter pelo menos $LENGTH$ caracteres.", + "message": "A palavra-passe mestra deve ter pelo menos $LENGTH$ carateres.", "placeholders": { "length": { "content": "$1", @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Dispositivos de confiança" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Uma vez autenticados, os membros desencriptam os dados do cofre utilizando uma chave armazenada no seu dispositivo. A", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Os membros não precisarão de uma palavra-passe mestra quando iniciarem sessão com o SSO. A palavra-passe mestra é substituída por uma chave de encriptação armazenada no dispositivo, tornando esse dispositivo fiável. O primeiro dispositivo em que um membro cria a sua conta e inicia sessão será de confiança. Os novos dispositivos terão de ser aprovados por um dispositivo de confiança existente ou por um administrador. A", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "política de organização", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "única,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "política de SSO", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "a política de SSO", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "obrigatória, e a", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "obrigatório e a", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "gestão da recuperação de contas", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "política de administração de recuperação", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "com inscrição automática será ativada quando esta opção for utilizada.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "de conta serão ativadas quando esta opção for utilizada.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "As permissões da sua organização foram atualizadas, exigindo a definição de uma palavra-passe mestra.", @@ -8362,7 +8501,7 @@ } }, "next": { - "message": "Avançar" + "message": "Seguinte" }, "ssoLoginIsRequired": { "message": "É necessário um início de sessão SSO" @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limitar a eliminação de coleções aos proprietários e administradores" }, + "limitItemDeletionDesc": { + "message": "Limitar a eliminação de itens a membros com a permissão Pode gerir" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Os proprietários e administradores podem gerir todas as coleções e itens" }, @@ -8544,9 +8686,6 @@ "message": "URL do servidor auto-hospedado", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias de domínio" - }, "alreadyHaveAccount": { "message": "Já tem uma conta?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Não tem acesso para gerir esta coleção." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Faltam permissões Pode gerir" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Conceda permissões Pode gerir para permitir a gestão completa da coleção, incluindo a eliminação da coleção." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Conceder a grupos ou membros acesso a esta coleção." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "mês por membro" }, + "monthPerMemberBilledAnnually": { + "message": "mês por membro faturado anualmente" + }, "seats": { "message": "Lugares" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Algoritmo de chaves" }, + "sshPrivateKey": { + "message": "Chave privada" + }, + "sshPublicKey": { + "message": "Chave pública" + }, + "sshFingerprint": { + "message": "Impressão digital" + }, "sshKeyFingerprint": { "message": "Impressão digital" }, @@ -9773,10 +9924,10 @@ "description": "The text, 'API', is an acronym and should not be translated." }, "showCharacterCount": { - "message": "Mostrar contagem de caracteres" + "message": "Mostrar contagem de carateres" }, "hideCharacterCount": { - "message": "Ocultar contagem de caracteres" + "message": "Ocultar contagem de carateres" }, "editAccess": { "message": "Editar acesso" @@ -9797,7 +9948,7 @@ "message": "Introduza o ID do HTML, o nome, a aria-label ou o placeholder do campo." }, "uppercaseDescription": { - "message": "Incluir caracteres em maiúsculas", + "message": "Incluir carateres em maiúsculas", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -9805,7 +9956,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Incluir caracteres em minúsculas", + "message": "Incluir carateres em minúsculas", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -9821,7 +9972,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Incluir caracteres especiais", + "message": "Incluir carateres especiais", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Código descritor" }, + "cannotRemoveViewOnlyCollections": { + "message": "Não é possível remover coleções com permissões de Apenas visualização: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Aviso importante" }, @@ -10077,9 +10237,9 @@ "message": "Domínio reivindicado" }, "organizationNameMaxLength": { - "message": "O nome da organização não pode exceder 50 caracteres." + "message": "O nome da organização não pode exceder 50 carateres." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "A sua subscrição será renovada em breve. Para garantir um serviço ininterrupto, contacte a $RESELLER$ para confirmar a sua renovação antes de $RENEWAL_DATE$.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "A fatura da sua subscrição foi emitida a $ISSUED_DATE$. Para garantir um serviço ininterrupto, contacte a $RESELLER$ para confirmar a sua renovação antes de $DUE_DATE$.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "A fatura da sua subscrição não foi paga. Para garantir um serviço ininterrupto, contacte a $RESELLER$ para confirmar a sua renovação antes de $GRACE_PERIOD_END$.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "Tem $REMAINING$ lugares restantes dos $TOTAL$ lugares atribuídos a esta organização. Contacte o seu fornecedor para gerir a sua subscrição.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Organização existente" + }, + "selectOrganizationProviderPortal": { + "message": "Selecione uma organização para adicionar ao seu Portal do fornecedor." + }, + "noOrganizations": { + "message": "Não existem organizações para listar" + }, + "yourProviderSubscriptionCredit": { + "message": "A subscrição do fornecedor receberá um crédito por qualquer tempo restante na subscrição da organização." + }, + "doYouWantToAddThisOrg": { + "message": "Pretende adicionar esta organização a $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Organização existente adicionada" + }, + "assignedExceedsAvailable": { + "message": "Os lugares atribuídos excedem os lugares disponíveis." } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 42d136884444..4db5a16a5661 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Ce fel de articol este acesta?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Domeniu de bază", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Autentificare inițiată" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Trimitere" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "O notificare a fost trimisă pe dispozitivul dvs." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Versiunea $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerare parolă" - }, "length": { "message": "Lungime" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Zona de pericol" }, - "dangerZoneDesc": { - "message": "Atenție, aceste acțiuni nu sunt reversibile!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Revocare sesiuni" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Procedând astfel, veți fi deconectat din sesiunea curentă, fiind necesar să vă reconectați. De asemenea, vi se va solicita din nou autentificarea în două etape, dacă este configurată. Sesiunile active de pe alte dispozitive pot rămâne active timp de până la o oră." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Toate sesiunile au fost dezautorizate" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Gestionare" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Dezactivare" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "A apărut o problemă la citirea cheii de securitate. Încercați din nou." }, - "twoFactorWebAuthnWarning": { - "message": "Din cauza limitărilor de platformă, WebAuthn nu poate fi utilizat pe toate aplicațiile Bitwarden. Ar trebui să configurați un alt furnizor de autentificare în două etape, astfel încât să vă puteți accesa contul atunci când WebAuthn nu se poate utiliza. Platformele acceptate:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Seif web și extensii de browser pe un desktop/laptop cu un browser compatibil cu WebAuthn (Chrome, Opera, Vivaldi sau Firefox cu FIDO U2F activat)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Codul dvs. Bitwarden de recuperare a autentificării în două etape" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Acest utilizator folosește conectarea în două etape pentru a-și proteja contul." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "SSO deconectat pentru utilizatorul $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Setați cerințele pentru generatorul de parole." }, - "passwordGeneratorPolicyInEffect": { - "message": "Una sau mai multe politici organizaționale vă afectează setările generatorului." - }, "masterPasswordPolicyInEffect": { "message": "Una sau mai multe politici organizaționale necesită ca parola principală să îndeplinească următoarele cerințe:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Ce doriți să generați?" - }, - "passwordType": { - "message": "Tip de parolă" - }, - "regenerateUsername": { - "message": "Regenerare nume de utilizator" - }, "generateUsername": { "message": "Generare nume de utilizator" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Tip de nume de utilizator" - }, "plusAddressedEmail": { "message": "E-mail Plus adresat", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Utilizați inbox-ul catch-all configurat pentru domeniul dvs." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Aleatoriu", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Nume gazdă", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Token de acces API" - }, "deviceVerification": { "message": "Verificarea dispozitivului" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 28386190d176..c8017041bad2 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Участники группы риска" }, + "atRiskMembersWithCount": { + "message": "Пользователи повышенного риска ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Приложения с повышенным риском ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Эти пользователи входят в приложения со слабыми, скомпрометированными или повторно используемыми паролями." + }, + "atRiskApplicationsDescription": { + "message": "Эти приложения имеют слабые, скомпрометированные или повторно используемые пароли." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Эти пользователи входят в $APPNAME$ со слабыми, скомпрометированными или повторно используемыми паролями.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Всего участников" }, @@ -122,11 +155,17 @@ "totalApplications": { "message": "Всего приложений" }, + "unmarkAsCriticalApp": { + "message": "Снять пометку критического приложения" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Критическое приложение успешно снято" + }, "whatTypeOfItem": { "message": "Выберите тип элемента" }, "name": { - "message": "Название" + "message": "Имя" }, "uri": { "message": "URI" @@ -425,6 +464,18 @@ "editFolder": { "message": "Изменить папку" }, + "newFolder": { + "message": "Новая папка" + }, + "folderName": { + "message": "Название папки" + }, + "folderHintText": { + "message": "Создайте вложенную папку, добавив название родительской папки и символ \"/\". Пример: Сообщества/Форумы" + }, + "deleteFolderPermanently": { + "message": "Вы действительно хотите безвозвратно удалить эту папку?" + }, "baseDomain": { "message": "Основной домен", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Название элемента" }, - "cannotRemoveViewOnlyCollections": { - "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "напр.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Подтвердите вашу личность" }, + "weDontRecognizeThisDevice": { + "message": "Мы не распознали это устройство. Введите код, отправленный на ваш email, чтобы подтвердить вашу личность." + }, + "continueLoggingIn": { + "message": "Продолжить вход" + }, "whatIsADevice": { "message": "Что такое устройство?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Вход инициирован" }, + "logInRequestSent": { + "message": "Запрос отправлен" + }, "submit": { "message": "Подтвердить" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "На ваше устройство отправлено уведомление." }, + "notificationSentDevicePart1": { + "message": "Разблокируйте Bitwarden на своем устройстве или " + }, + "areYouTryingToAccessYourAccount": { + "message": "Вы пытаетесь получить доступ к своему аккаунту?" + }, + "accessAttemptBy": { + "message": "Попытка доступа $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Подтвердить доступ" + }, + "denyAccess": { + "message": "Отказать в доступе" + }, + "notificationSentDeviceAnchor": { + "message": "веб-приложении" + }, + "notificationSentDevicePart2": { + "message": "Перед одобрением убедитесь, что фраза отпечатка совпадает с приведенной ниже." + }, + "notificationSentDeviceComplete": { + "message": "Разблокируйте Bitwarden на своем устройстве. Перед одобрением убедитесь, что фраза отпечатка совпадает с приведенной ниже." + }, "aNotificationWasSentToYourDevice": { "message": "На ваше устройство было отправлено уведомление" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Убедитесь, что ваш аккаунт разблокирован и фраза отпечатка совпадает с фразой на другом устройстве" - }, "versionNumber": { "message": "Версия $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Избегать неоднозначных символов", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Создать новый пароль" - }, "length": { "message": "Длина" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Зона риска" }, - "dangerZoneDesc": { - "message": "Осторожно, эти действия необратимы!" - }, - "dangerZoneDescSingular": { - "message": "Будьте внимательны - это действие не обратимо!" - }, "deauthorizeSessions": { "message": "Деавторизовать сессии" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "В случае продолжения, ваша сессия будет завершена и вам будет предложено авторизоваться повторно. Вам также будет предложено выполнить двухэтапную аутентификацию, если она настроена. Сессии на других устройствах могут оставаться активными в течение одного часа." }, + "newDeviceLoginProtection": { + "message": "Авторизация с нового устройства" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Отключить защиту авторизации с нового устройства" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Включить защиту авторизации с нового устройства" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Продолжите, чтобы отключить письма от bitwarden отправляемые при авторизации с нового устройства." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Продолжите, чтобы включить письма от bitwarden отправляемые при авторизации с нового устройства." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Если защита авторизации с нового устройства отключена, любой с вашим мастер-паролем может получить доступ к вашему аккаунту с любого устройства. Чтобы обезопасить аккаунт при отключении писем от bitwarden настройте двухэтапную аутентификацию." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Изменения защиты авторизации сохранены" + }, "sessionsDeauthorized": { "message": "Все сессии деавторизованы" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Управление" }, - "canManage": { - "message": "Может управлять" + "manageCollection": { + "message": "Управление коллекцией" + }, + "viewItems": { + "message": "Просмотр элементов" + }, + "viewItemsHidePass": { + "message": "Просмотр элементов, скрытых паролей" + }, + "editItems": { + "message": "Изменить элементы" + }, + "editItemsHidePass": { + "message": "Изменить элементы, скрытые пароли" }, "disable": { "message": "Отключить" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Не удалось прочитать ключ безопасности. Попробуйте снова." }, - "twoFactorWebAuthnWarning": { - "message": "Из-за ограничений платформы WebAuthn нельзя использовать во всех приложениях Bitwarden. Вы должны настроить другого провайдера двухэтапной аутентификации, чтобы иметь возможность получить доступ к своей учетной записи, когда WebAuthn не может быть использован. Поддерживаемые платформы:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Веб-хранилище и расширения браузера на компьютере/ноутбуке с браузером с поддержкой WebAuthn (Chrome, Opera, Vivaldi или Firefox с включенным FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Из-за ограничений платформы WebAuthn нельзя использовать во всех приложениях Bitwarden. Вы должны настроить другого провайдера двухэтапной аутентификации, чтобы иметь возможность получить доступ к своей учетной записи, когда WebAuthn не может быть использован." }, "twoFactorRecoveryYourCode": { "message": "Ваш код восстановления двухэтапной аутентификации Bitwarden" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "У вас осталось 1 приглашение." }, + "inviteZeroEmailDesc": { + "message": "У вас осталось 0 приглашений." + }, "userUsingTwoStep": { "message": "Этот пользователь использует двухэтапную аутентификацию для защиты своего аккаунта." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Несвязанный SSO." + }, "unlinkedSsoUser": { "message": "Несвязанный SSO для пользователя $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Доверенный" }, + "needsApproval": { + "message": "Требуется одобрение" + }, + "areYouTryingtoLogin": { + "message": "Вы пытаетесь войти?" + }, + "logInAttemptBy": { + "message": "Попытка авторизации через $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Тип устройства" + }, + "ipAddress": { + "message": "IP-адрес" + }, + "confirmLogIn": { + "message": "Подтвердить вход" + }, + "denyLogIn": { + "message": "Запретить вход" + }, + "thisRequestIsNoLongerValid": { + "message": "Этот запрос больше не действителен." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Вход подтвержден для $EMAIL$ на $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Вы отклонили попытку авторизации с другого устройства. Если это действительно были вы, попробуйте авторизоваться с этого устройства еще раз." + }, + "loginRequestHasAlreadyExpired": { + "message": "Запрос на вход истек." + }, + "justNow": { + "message": "Только что" + }, + "requestedXMinutesAgo": { + "message": "Запрошено $MINUTES$ мин назад", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Создание аккаунта" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Установить требования к генератору паролей." }, - "passwordGeneratorPolicyInEffect": { - "message": "На настройки генератора влияют одна или несколько политик организации." - }, "masterPasswordPolicyInEffect": { "message": "Согласно одной или нескольким политикам организации необходимо, чтобы ваш мастер-пароль отвечал следующим требованиям:" }, @@ -6554,15 +6717,6 @@ "message": "Генератор", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Что вы хотите сгенерировать?" - }, - "passwordType": { - "message": "Тип пароля" - }, - "regenerateUsername": { - "message": "Пересоздать имя пользователя" - }, "generateUsername": { "message": "Создать имя пользователя" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Тип имени пользователя" - }, "plusAddressedEmail": { "message": "Субадресованные email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Использовать общий email домена." }, + "useThisEmail": { + "message": "Использовать этот email" + }, "random": { "message": "Случайно", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Имя хоста", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Токен доступа к API" - }, "deviceVerification": { "message": "Проверка устройства" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Нет коллекций" }, - "canView": { - "message": "Может просматривать" - }, - "canViewExceptPass": { - "message": "Может просматривать, кроме паролей" - }, - "canEdit": { - "message": "Может редактировать" - }, - "canEditExceptPass": { - "message": "Может редактировать, кроме паролей" - }, "noCollectionsAdded": { "message": "Нет добавленных коллекций" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Доверенные устройства" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "После аутентификации данные хранилища пользователей станут расшифровываться с помощью ключа, хранящегося на их устройстве. При", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Пользователям не понадобится мастер-пароль при входе в систему с SSO. Мастер-пароль заменяется ключом шифрования, хранящимся на устройстве, который делает его надёжным. Первое устройство, в которое участник создаёт свой аккаунт и входит в первый раз, будет доверенным. Новые устройства должны быть утверждены существующим доверенным устройством или администратором.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "использовании данной опции будут включены политика единой организации,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "одна организация", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "политика", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "политика,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "Требуется SSO", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "и политика", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "политика, и", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "администрирования восстановления аккаунтов", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "с автоматической регистрацией.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Права доступа организации были обновлены, требуется установить мастер-пароль.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Ограничить удаление коллекций владельцам и администраторам" }, + "limitItemDeletionDesc": { + "message": "Ограничить удаление элементов для пользователей с разрешением 'Может управлять'" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Владельцы и администраторы могут управлять всеми коллекциями и элементами" }, @@ -8544,9 +8686,6 @@ "message": "URL собственного сервера", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Псевдоним домена" - }, "alreadyHaveAccount": { "message": "Уже зарегистрированы?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "У вас нет доступа к управлению этой коллекцией." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Недостаточно полномочий для управления" + "grantManageCollectionWarningTitle": { + "message": "Отсутствуют разрешения на управление коллекцией" }, - "grantAddAccessCollectionWarning": { - "message": "Предоставить возможность управлять разрешениями коллекций, включая их удаление." + "grantManageCollectionWarning": { + "message": "Представить разрешения на управление коллекцией, включая ее удаление." }, "grantCollectionAccess": { "message": "Предоставить группам или участникам доступ к этой коллекции." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "в месяц за пользователя" }, + "monthPerMemberBilledAnnually": { + "message": "месяц за пользователя в год" + }, "seats": { "message": "Места" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Алгоритм ключа" }, + "sshPrivateKey": { + "message": "Приватный ключ" + }, + "sshPublicKey": { + "message": "Публичный ключ" + }, + "sshFingerprint": { + "message": "Отпечаток" + }, "sshKeyFingerprint": { "message": "Отпечаток" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Код дескриптора" }, + "cannotRemoveViewOnlyCollections": { + "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Важное уведомление" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Название организации не может превышать 50 символов." }, - "resellerRenewalWarning": { - "message": "Ваша подписка скоро будет продлена. Чтобы гарантировать непрерывность сервиса, свяжитесь с $RESELLER$ для подтверждения продления до $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Ваша подписка скоро будет продлена. Чтобы обеспечить непрерывность сервиса, свяжитесь с $RESELLER$ для подтверждения продления до $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "Счет за вашу подписку был выставлен $ISSUED_DATE$. Чтобы гарантировать непрерывность сервиса, свяжитесь с $RESELLER$, чтобы подтвердить продление подписки до $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "Счет за вашу подписку был выставлен $ISSUED_DATE$. Чтобы обеспечить непрерывность сервиса, свяжитесь с $RESELLER$, чтобы подтвердить продление подписки до $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "Счет за вашу подписку не был оплачен. Чтобы гарантировать непрерывность сервиса, свяжитесь с $RESELLER$ для подтверждения продления до $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "Счет за вашу подписку не был оплачен. Чтобы обеспечить непрерывность сервиса, свяжитесь с $RESELLER$ для подтверждения продления до $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Администраторы теперь могут удалять аккаунты пользователей, принадлежащие заявленному домену." + }, + "deleteManagedUserWarningDesc": { + "message": "Это действие удалит аккаунт пользователя, включая все элементы в его хранилище. Это действие заменяет предыдущее действие Удалить." + }, + "deleteManagedUserWarning": { + "message": "Удалить - это новое действие!" + }, + "seatsRemaining": { + "message": "У вас осталось $REMAINING$ мест из $TOTAL$, закрепленных за этой организацией. Обратитесь к своему провайдеру для управления подпиской.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Существующая организация" + }, + "selectOrganizationProviderPortal": { + "message": "Выберите организацию для добавления на ваш портал провайдеров." + }, + "noOrganizations": { + "message": "Нет организаций для отображения" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Вы хотите добавить эту организацию в $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index df5803c0fd9a..058528cb18d7 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "උදා.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index d8adb9116ec7..bee2d072f91b 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Ohrozených členov" }, + "atRiskMembersWithCount": { + "message": "Ohrození členovia ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Ohrozené aplikácie ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Títo členovia sa prihlasujú do aplikácií so slabým, uniknutým alebo viacnásobne použitým heslom." + }, + "atRiskApplicationsDescription": { + "message": "Tieto aplikácie majú slabé, uniknuté alebo opätovne použité heslá." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Títo členovia sa prihlasujú do $APPNAME$ so slabým, uniknutým alebo viacnásobne použitým heslom.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Celkový počet členov" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Všetkých aplikácii" }, + "unmarkAsCriticalApp": { + "message": "Zrušiť označenie aplikácie za kritickú" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Označenie aplikácie za kritickú úspešne zrušené" + }, "whatTypeOfItem": { "message": "Aký typ položky to je?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Naozaj chcete natrvalo odstrániť tento priečinok?" + }, "baseDomain": { "message": "Základná doména", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Názov položky" }, - "cannotRemoveViewOnlyCollections": { - "message": "Zbierky, ktoré môžete len zobraziť nemôžete odstrániť: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "napr.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Overte svoju totožnosť" }, + "weDontRecognizeThisDevice": { + "message": "Nespoznávame toto zariadenie. Pre overenie vašej identity zadajte kód ktorý bol zaslaný na váš email." + }, + "continueLoggingIn": { + "message": "Pokračovať v prihlasovaní" + }, "whatIsADevice": { "message": "Čo je zariadenie?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Iniciované prihlásenie" }, + "logInRequestSent": { + "message": "Požiadavka bola odoslaná" + }, "submit": { "message": "Potvrdiť" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Do vášho zariadenia bolo odoslané upozornenie." }, + "notificationSentDevicePart1": { + "message": "Odomknúť Bitwarden vo svojom zariadení alebo vo " + }, + "areYouTryingToAccessYourAccount": { + "message": "Snažíte sa získať prístup k svojmu účtu?" + }, + "accessAttemptBy": { + "message": "Pokus o prístup z $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Potvrďte prístup" + }, + "denyAccess": { + "message": "Zamietnuť prístup" + }, + "notificationSentDeviceAnchor": { + "message": "webovej aplikácii" + }, + "notificationSentDevicePart2": { + "message": "Pred schválením sa uistite, že sa odtlačok prístupovej frázy zhoduje s tou uvedenou nižšie." + }, + "notificationSentDeviceComplete": { + "message": "Odomknite Bitwarden na vašom zariadení. Pred schválením sa uistite, že sa odtlačok prístupovej frázy zhoduje s tým uvedeným nižšie." + }, "aNotificationWasSentToYourDevice": { "message": "Do vášho zariadenia bolo odoslané upozornenie" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Uistite sa, že je váš účet odomknutý a fráza odtlačku prsta sa zhoduje s frázou na druhom zariadení" - }, "versionNumber": { "message": "Verzia $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Vyhnúť sa zameniteľným znakom", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Vygenerovať nové heslo" - }, "length": { "message": "Dĺžka" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Riziková zóna" }, - "dangerZoneDesc": { - "message": "Opatrne, tieto zmeny nemožno vrátiť späť!" - }, - "dangerZoneDescSingular": { - "message": "Opatrne, túto zmenu nemožno vrátiť späť!" - }, "deauthorizeSessions": { "message": "Odhlásiť sedenia" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Ak budete pokračovať, budete tiež odhlásený z vášho súčasného sedenia a budete sa musieť opäť prihlásiť. Tiež budete opäť požiadaný o dvojstupňové prihlásenie ak ho máte zapnuté. Aktívne sedenia na iných zariadeniach môžu ostať aktívne až po dobu jednej hodiny." }, + "newDeviceLoginProtection": { + "message": "Prihlásenie z nového zariadenia" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Vypnúť ochranu pri prihlásení z nového zariadenia" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Zapnúť ochranu pri prihlásení z nového zariadenia" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Ak chcete vypnúť overovacie emaily, ktoré Bitwarden posiela keď sa prihlasujete z nového zariadenia, pokračujte nižšie." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Ak chcete aby Bitwarden posielal overovacie emaily keď sa prihlasujete z nového zariadenia, pokračujte nižšie." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "S vypnutou ochranou pri prihlásení z nového zariadenia môže ktokoľvek, kto pozná vaše hlavne heslo, pristupovať k vášmu účtu z ľubovoľného zariadenia. Ak chcete ochrániť váš účet bez overovacích emailov, nastavte si dvojstupňové overovanie pri prihlasovaní." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Zmeny v nastavení ochrany prihlasovania z nového zariadenia boli uložené" + }, "sessionsDeauthorized": { "message": "Všetky sedenia odhlásené" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Spravovať" }, - "canManage": { - "message": "Môže spravovať" + "manageCollection": { + "message": "Spravovať zbierku" + }, + "viewItems": { + "message": "Zobraziť položky" + }, + "viewItemsHidePass": { + "message": "Zobraziť položky, skryté heslá" + }, + "editItems": { + "message": "Upraviť položky" + }, + "editItemsHidePass": { + "message": "Upraviť položky, skryté heslá" }, "disable": { "message": "Vypnúť" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Problém pri čítaní bezpečnostného kľúča. Skúste to znova." }, - "twoFactorWebAuthnWarning": { - "message": "Vzhľadom na obmedzenia platform, WebAuthn nemôže byť použitý vo všetkých Bitwarden aplikáciách. Mali by ste povoliť inú formu dvojitého overenia, aby ste sa mohli prihlásiť k svojmu účtu ak nie je možné použiť WebAuthn. Podporované platformy:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Webový trezor a rozšírenia prehliadača na pracovnej stanici s prehliadačom podporujúcim WebAuthn (Chrome, Opera, Vivaldi, alebo Firefox so zapnutou podporou FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Vzhľadom na obmedzenia platforiem, WebAuthn nemôže byť použitý vo všetkých Bitwarden aplikáciách. Mali by ste nastaviť inú formu dvojitého overenia, aby ste sa mohli prihlásiť k svojmu účtu ak nie je možné použiť WebAuthn." }, "twoFactorRecoveryYourCode": { "message": "Váš Bitwarden záchranný kód pre dvojstupňové overovanie" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "Ostáva vám 1 pozvánka." }, + "inviteZeroEmailDesc": { + "message": "Ostáva vám 0 pozvánok." + }, "userUsingTwoStep": { "message": "Tento používateľ používa dvojstupňové overovanie aby si zabezpečil konto." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Odpojené SSO." + }, "unlinkedSsoUser": { "message": "SSO odpojené pre používateľa $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Dôveryhodné" }, + "needsApproval": { + "message": "Potrebuje súhlas" + }, + "areYouTryingtoLogin": { + "message": "Snažíte sa prihlásiť?" + }, + "logInAttemptBy": { + "message": "Pokus o prihlásenie pomocou $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Typ zariadenia" + }, + "ipAddress": { + "message": "IP adresa" + }, + "confirmLogIn": { + "message": "Potvrdiť prihlásenie" + }, + "denyLogIn": { + "message": "Odmietnuť prihlásenie" + }, + "thisRequestIsNoLongerValid": { + "message": "Táto žiadosť už nie je platná." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Potvrdené prihlásenie pomocou $EMAIL$ na $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Odmietli ste pokus o prihlásenie z iného zariadenia. Ak ste to boli naozaj vy, skúste sa prihlásiť pomocou zariadenia znova." + }, + "loginRequestHasAlreadyExpired": { + "message": "Platnosť žiadosti o prihlásenie už vypršala." + }, + "justNow": { + "message": "Práve teraz" + }, + "requestedXMinutesAgo": { + "message": "Vyžiadané pred $MINUTES$ minútami", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Zvoľte minimálne požiadavky pre nastavenie generátora hesiel." }, - "passwordGeneratorPolicyInEffect": { - "message": "Jedno alebo viac nastavení organizácie ovplyvňujú vaše nastavenia generátora." - }, "masterPasswordPolicyInEffect": { "message": "Jedno alebo viac pravidiel organizácie požadujú aby vaše hlavné heslo spĺňalo nasledujúce požiadavky:" }, @@ -6554,15 +6717,6 @@ "message": "Generátor", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Čo by ste chceli vygenerovať?" - }, - "passwordType": { - "message": "Typ hesla" - }, - "regenerateUsername": { - "message": "Vygenerovať nové používateľské meno" - }, "generateUsername": { "message": "Vygenerovať používateľské meno" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Typ používateľského mena" - }, "plusAddressedEmail": { "message": "E-mail s plusovým aliasom", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Použiť doručenú poštu typu catch-all nastavenú na doméne." }, + "useThisEmail": { + "message": "Použiť tento e-mail" + }, "random": { "message": "Náhodné", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Názov hostiteľa", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Prístupový token API" - }, "deviceVerification": { "message": "Overenie zariadenia" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Žiadna zbierka" }, - "canView": { - "message": "Môže zobraziť" - }, - "canViewExceptPass": { - "message": "Môže zobraziť okrem hesiel" - }, - "canEdit": { - "message": "Môže upraviť" - }, - "canEditExceptPass": { - "message": "Môže upravovať okrem hesiel" - }, "noCollectionsAdded": { "message": "Nebolí pridané žiadne zbierky" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Dôveryhodné zariadenia" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Po autentifikácii, členovia budú dešifrovať dáta v trezore za pomoci kľúča uloženého na ich zariadení.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "jedna organizácia", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "správa obnovy konta", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Povolenia vašej organizácie boli aktualizované, čo si vyžaduje nastavenie hlavného hesla.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Obmedziť vymazávanie zbierky len pre vlastníkov a administrátorov" }, + "limitItemDeletionDesc": { + "message": "Obmedziť vymazávanie položiek na členov s oprávnením \"Môže spravovať\"" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Vlastníci a správcovia môžu spravovať všetky zbierky a položky" }, @@ -8544,9 +8686,6 @@ "message": "Adresa URL vlastného hostingu", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias doména" - }, "alreadyHaveAccount": { "message": "Už máte účet?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Nemáte prístup k spravovaniu tejto zbierky." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Chýbajúce oprávnenia pre správu zbierky" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Pre povolenie kompletnej správy zbierky vrátane jej vymazania, udeľte oprávnenia na správu zbierky." }, "grantCollectionAccess": { "message": "Povoľte skupinám, alebo jednotlivcom prístup k tejto zbierke." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "mesačne za člena účtovaného ročne" + }, "seats": { "message": "Sedenia" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Algoritmus kľúča" }, + "sshPrivateKey": { + "message": "Súkromný kľúč" + }, + "sshPublicKey": { + "message": "Verejný kľúč" + }, + "sshFingerprint": { + "message": "Odtlačok" + }, "sshKeyFingerprint": { "message": "Odtlačok" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Kód výpisu" }, + "cannotRemoveViewOnlyCollections": { + "message": "Zbierky, ktoré môžete len zobraziť nemôžete odstrániť: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Dôležité upozornenie" }, @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Meno organizácie nemôže mať viac ako 50 znakov." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Vaše predplatné sa čoskoro obnoví. Aby ste si zabezpečili nepretržitú prevádzku, kontaktujte $RESELLER$ a potvrďte obnovenie pred $RENEWAL_DATE$.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Faktúra za vaše predplatné bola vystavená dňa $ISSUED_DATE$. Aby ste si zabezpečili nepretržitú prevádzku, kontaktujte $RESELLER$ a potvrďte obnovenie predplatného pred $DUE_DATE$.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Faktúra za vaše predplatné nebola uhradená. Aby ste si zabezpečili nepretržitú prevádzku, kontaktujte $RESELLER$ a potvrďte obnovenie pred $GRACE_PERIOD_END$.", "placeholders": { "reseller": { @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Odteraz majú správcovia právomoc vymazať konta členov, ktorí prináležia do privlastnenej domény." + }, + "deleteManagedUserWarningDesc": { + "message": "Táto akcia vymaže člena vrátane všetkých ich položiek v trezore. Tato akcia nahrádza predchádzajúcu akciu Odstrániť." + }, + "deleteManagedUserWarning": { + "message": "\"Vymazať\" je nová akcia!" + }, + "seatsRemaining": { + "message": "Z celkovo $TOTAL$ sedení pridelených tejto organizácii vám zostava $REMAINING$ sedení. Pre správu predplatného kontaktujte vášho poskytovateľa.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existujúca organizácia" + }, + "selectOrganizationProviderPortal": { + "message": "Vyberte organizáciu ktorú chcete pridať do portálu poskytovateľa." + }, + "noOrganizations": { + "message": "Neexistujú žiadne organizácie na zobrazenie" + }, + "yourProviderSubscriptionCredit": { + "message": "Váš poskytovateľ predplatného obdrží kredit za zostávajúci čas predplatného vašej organizácie." + }, + "doYouWantToAddThisOrg": { + "message": "Chcete pridať tuto organizáciu k $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Existujúca organizácia pridaná" + }, + "assignedExceedsAvailable": { + "message": "Počet pridelených sedení presahuje počet dostupných sedení." } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 75ad87494f06..c440f0cfcf17 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -1,18 +1,18 @@ { "allApplications": { - "message": "All applications" + "message": "Vse aplikacije" }, "criticalApplications": { - "message": "Critical applications" + "message": "Kritične aplikacije" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "Analiza dostopa" }, "riskInsights": { - "message": "Risk Insights" + "message": "Vpogled v tveganja" }, "passwordRisk": { - "message": "Password Risk" + "message": "Varnostno tveganje gesla" }, "reviewAtRiskPasswords": { "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Katere vrste element je to?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Bazna domena", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "npr.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Prijava se je začela" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Potrdi" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Na vašo napravo smo poslali obvestilo." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Različica $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Ponovno generiraj geslo" - }, "length": { "message": "Dolžina" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Območje nevarnosti" }, - "dangerZoneDesc": { - "message": "Previdno, ta dejanja so nepovratna!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Prekini seje" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Vse seje prekinjene" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Upravljaj" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Onemogočeno" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Kaj želite generirati?" - }, - "passwordType": { - "message": "Vrsta gesla" - }, - "regenerateUsername": { - "message": "Ponovno generiraj uporabniško ime" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Vrsta uporabniškega imena" - }, "plusAddressedEmail": { "message": "E-naslov s plusom", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Uporabite naslov za vse (\"catch-all\"), ki ste ga nastavili za svojo domeno." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Dostopni žeto (\"access token\") za API" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 0f3853331c2e..809fa21d8521 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -15,7 +15,7 @@ "message": "Ризик од лозинке" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Прегледај ризичне лозинке (слабе, изложене или поново коришћене) у апликацијама. Изабери своје најкритичније апликације да би дао приоритет безбедносним радњама како би твоји корисници адресирали ризичне лозинке." }, "dataLastUpdated": { "message": "Подаци су последњи пут ажурирани: $DATE$", @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Чланови под ризиком" }, + "atRiskMembersWithCount": { + "message": "Ризични чланови ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Ризичне апликације ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Ови чланови се пријављују у апликације са слабим, откривеним или поново коришћеним лозинкама." + }, + "atRiskApplicationsDescription": { + "message": "Ове апликације имају слабу, проваљену или често коришћену лозинку." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Ови чланови се пријављују у $APPNAME$ са слабим, откривеним или поново коришћеним лозинкама.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Укупно чланова" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Укупно апликација" }, + "unmarkAsCriticalApp": { + "message": "Уклони ознаку критичне апликације" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Успешно уклоњена ознака са критичне апликације" + }, "whatTypeOfItem": { "message": "Који је ово тип елемента?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Главни домен", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Име ставке" }, - "cannotRemoveViewOnlyCollections": { - "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "нпр.", "description": "Short abbreviation for 'example'." @@ -777,7 +819,7 @@ "message": "Копирати телефон" }, "copyEmail": { - "message": "Копирати имејл" + "message": "Копирај Е-пошту" }, "copyCompany": { "message": "Копирати фирму" @@ -1128,20 +1170,29 @@ "verifyIdentity": { "message": "Потврдите идентитет" }, + "weDontRecognizeThisDevice": { + "message": "Не препознајемо овај уређај. Унесите код послат на адресу ваше електронске поште да би сте потврдили ваш идентитет." + }, + "continueLoggingIn": { + "message": "Настави са пријављивањем" + }, "whatIsADevice": { - "message": "What is a device?" + "message": "Шта је уређај?" }, "aDeviceIs": { - "message": "A device is a unique installation of the Bitwarden app where you have logged in. Reinstalling, clearing app data, or clearing your cookies could result in a device appearing multiple times." + "message": "Уређај је јединствена инсталација Bitwarden апликације на коју сте се пријавили. Поновно инсталирање, брисање података апликације или брисање колачића може довести до тога да се уређај појави више пута." }, "logInInitiated": { "message": "Пријава је покренута" }, + "logInRequestSent": { + "message": "Захтев је послат" + }, "submit": { "message": "Пошаљи" }, "emailAddressDesc": { - "message": "Користите ваш имејл за пријављивање." + "message": "Користи твоју Е-пошту за пријављивање." }, "yourName": { "message": "Ваше име" @@ -1174,7 +1225,7 @@ "message": "Савет Главне Лозинке" }, "masterPassHintText": { - "message": "Ако заборавите лозинку, наговештај за лозинку се може послати на ваш имејл. $CURRENT$/$MAXIMUM$ карактера максимум.", + "message": "Ако заборавиш лозинку, наговештај за лозинку се може послати на твоју Е-пошту. $CURRENT$/$MAXIMUM$ карактера максимум.", "placeholders": { "current": { "content": "$1", @@ -1190,7 +1241,7 @@ "message": "Подешавања" }, "accountEmail": { - "message": "Имејл налога" + "message": "Е-пошта налога" }, "requestHint": { "message": "Захтевај савет" @@ -1199,22 +1250,22 @@ "message": "Затражити савет лозинке" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Унесите имејл свог налога и биће вам послат савет за лозинку" + "message": "Унеси адресу Е-поште свог налога и биће ти послат савет за лозинку" }, "passwordHint": { "message": "Помоћ за лозинку" }, "enterEmailToGetHint": { - "message": "Унесите Ваш имејл да би добили савет за Вашу Главну Лозинку." + "message": "Унеси твоју Е-пошту да би добио савет за твоју Главну Лозинку." }, "getMasterPasswordHint": { "message": "Добити савет за Главну Лозинку" }, "emailRequired": { - "message": "Имејл је неопходан." + "message": "Адреса Е-поште је неопходна." }, "invalidEmail": { - "message": "Неисправан имејл." + "message": "Нетачна адреса Е-поште." }, "masterPasswordRequired": { "message": "Главна Лозинка је неопходна." @@ -1257,7 +1308,7 @@ "message": "Изаберите датум истека који је у будућности." }, "emailAddress": { - "message": "Имејл" + "message": "Адреса Е-поште" }, "yourVaultIsLockedV2": { "message": "Ваш сеф је блокиран" @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Обавештење је послато на ваш уређај." }, + "notificationSentDevicePart1": { + "message": "Откључај Bitwarden на твом уређају или на " + }, + "areYouTryingToAccessYourAccount": { + "message": "Да ли покушавате да приступите вашем налогу?" + }, + "accessAttemptBy": { + "message": "Покушај приступа са $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Потврди приступ" + }, + "denyAccess": { + "message": "Одбиј приступ" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Потврдите да се фраза отиска прста поклапа са овом испод пре одобравања." + }, + "notificationSentDeviceComplete": { + "message": "Октључај Bitwarden на твом уређају. Потврдите да се фраза отиска прста поклапа са овом испод пре одобравања." + }, "aNotificationWasSentToYourDevice": { "message": "Обавештење је послато на ваш уређај" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Уверите се да је ваш налог откључан и да се фраза отиска подудара на другом уређају" - }, "versionNumber": { "message": "Верзија $VERSION_NUMBER$", "placeholders": { @@ -1354,7 +1432,7 @@ } }, "verificationCodeEmailSent": { - "message": "Провера имејла послата на $EMAIL$.", + "message": "Е-пошта за верификацију је послата на $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -1366,7 +1444,7 @@ "message": "Запамти ме" }, "sendVerificationCodeEmailAgain": { - "message": "Поново послати верификациони код на имејл" + "message": "Поново послати верификациони код на Е-пошту" }, "useAnotherTwoStepMethod": { "message": "Користите другу методу пријављивања у два корака" @@ -1619,9 +1697,6 @@ "message": "Избегавај двосмислене карактере", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Поново генериши лозинку" - }, "length": { "message": "Дужина" }, @@ -1722,10 +1797,10 @@ "message": "Молимо да се поново пријавите." }, "currentSession": { - "message": "Current session" + "message": "Тренутна сесија" }, "requestPending": { - "message": "Request pending" + "message": "Захтев је на чекању" }, "logBackInOthersToo": { "message": "Молимо вас да се поново пријавите. Ако користите друге Bitwarden апликације, одјавите се и вратите се и на њих." @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Опасна зона" }, - "dangerZoneDesc": { - "message": "Пажљиво, ове акције су крајне!" - }, - "dangerZoneDescSingular": { - "message": "Пажљиво, ова акција није реверзибилна!" - }, "deauthorizeSessions": { "message": "Одузели овлашћење сесије" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Наставак ће вас такође одјавити из тренутне сесије, што захтева поновно пријављивање. Од вас ће такође бити затражено да се поново пријавите у два корака, ако је омогућено. Активне сесије на другим уређајима могу да остану активне још један сат." }, + "newDeviceLoginProtection": { + "message": "Пријава на новом уређају" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Искључи заштиту пријаве на новом уређају" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Укључи заштиту пријаве на новом уређају" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Одузето овлашћење свих сесија" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Управљати" }, - "canManage": { - "message": "Може управљати" + "manageCollection": { + "message": "Управљај колекцијом" + }, + "viewItems": { + "message": "Прикажи ставке" + }, + "viewItemsHidePass": { + "message": "Прикажи ставке, сакривене лозинке" + }, + "editItems": { + "message": "Уреди ставке" + }, + "editItemsHidePass": { + "message": "Уреди ставке, сакривене лозинке" }, "disable": { "message": "Онемогући" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Догодила се грешка приликом читања безбедносног кључа. Покушајте поново." }, - "twoFactorWebAuthnWarning": { - "message": "Због ограничења платформе, WebAuthn се не могу користити на свим Bitwarden апликацијама. Требали бисте омогућити другог добављача услуге пријављивања у два корака како бисте могли да приступите свом налогу када WebAuthn не могу да се користе. Подржане платформе:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Веб сеф и додатке прегледача на рачунару са WebAuthn омогућен прегледач (Chrome, Opera, Vivaldi, или Firefox са FIDO U2F омогућено)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Ваш Bitwarden код за опоравак пријаве у два корака" @@ -3279,7 +3378,10 @@ } }, "inviteSingleEmailDesc": { - "message": "You have 1 invite remaining." + "message": "Преостала вам је 1 позивница." + }, + "inviteZeroEmailDesc": { + "message": "Преостало вам је 0 позивница." }, "userUsingTwoStep": { "message": "Овај корисник користи пријаву у два корака за заштиту свог налога." @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Неповезан SSO." + }, "unlinkedSsoUser": { "message": "Отповезај SSO за $ID$.", "placeholders": { @@ -3781,13 +3886,74 @@ "message": "Уређај" }, "loginStatus": { - "message": "Login status" + "message": "Статус пријаве" }, "firstLogin": { - "message": "First login" + "message": "Прва пријава" }, "trusted": { - "message": "Trusted" + "message": "Поуздан" + }, + "needsApproval": { + "message": "Потребно је одобрење" + }, + "areYouTryingtoLogin": { + "message": "Да ли покушавате да се пријавите?" + }, + "logInAttemptBy": { + "message": "Покушај пријаве од $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Тип уређаја" + }, + "ipAddress": { + "message": "ИП адреса" + }, + "confirmLogIn": { + "message": "Потврди пријављивање" + }, + "denyLogIn": { + "message": "Одбиј пријављивање" + }, + "thisRequestIsNoLongerValid": { + "message": "Овај захтев више није важећи." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Пријава потврђена за $EMAIL$ на $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Одбили сте покушај пријаве са другог уређаја. Ако сте то заиста били ви, покушајте поново да се пријавите помоћу уређаја." + }, + "loginRequestHasAlreadyExpired": { + "message": "Захтев за пријаву је већ истекао." + }, + "justNow": { + "message": "Управо сада" + }, + "requestedXMinutesAgo": { + "message": "Затражено пре $MINUTES$ минута", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } }, "creatingAccountOn": { "message": "Креирај налог на" @@ -3907,7 +4073,7 @@ "message": "Ажурирајте Претраживач" }, "generatingRiskInsights": { - "message": "Generating your risk insights..." + "message": "Генерисање прегледа вашег ризика..." }, "updateBrowserDesc": { "message": "Користите неподржани веб прегледач. Веб сеф можда неће правилно функционисати." @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Поставите минималне захтеве за конфигурацију генератора лозинки." }, - "passwordGeneratorPolicyInEffect": { - "message": "Једна или више смерница организације утичу на поставке вашег генератора." - }, "masterPasswordPolicyInEffect": { "message": "Једна или више смерница организације захтевају да ваша главна лозинка да би испуњавали следеће захтеве:" }, @@ -4749,7 +4912,7 @@ "message": "Пријавите се помоћу портала за јединствену пријаву ваше организације. Унесите идентификатор организације да бисте започели." }, "singleSignOnEnterOrgIdentifier": { - "message": "Enter your organization's SSO identifier to begin" + "message": "За почетак унесите SSO идентификатор ваше организације" }, "singleSignOnEnterOrgIdentifierText": { "message": "To log in with your SSO provider, enter your organization's SSO identifier to begin. You may need to enter this SSO identifier when you log in from a new device." @@ -5680,17 +5843,17 @@ "message": "Грешка" }, "decryptionError": { - "message": "Decryption error" + "message": "Грешка при декрипцији" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden није могао да декриптује ставке из трезора наведене испод." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Обратите се корисничкој подршци", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "да бисте избегли додатни губитак података.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -6024,10 +6187,10 @@ "message": "Assertion consumer service (ACS) URL" }, "spNameIdFormat": { - "message": "Name ID format" + "message": "Формат ИД назива" }, "spOutboundSigningAlgorithm": { - "message": "Outbound signing algorithm" + "message": "Алгоритам за излазно потписивање" }, "spSigningBehavior": { "message": "Понашање пријављања" @@ -6069,7 +6232,7 @@ "message": "Дозволи нежељени одговор на аутентификацију" }, "idpAllowOutboundLogoutRequests": { - "message": "Allow outbound logout requests" + "message": "Дозволи одлазне захтеве за одјављивањем" }, "idpSignAuthenticationRequests": { "message": "Sign authentication requests" @@ -6165,7 +6328,7 @@ } }, "freeFamiliesPlan": { - "message": "Free Families plan" + "message": "Бесплатан породични план" }, "redeemNow": { "message": "Откупи сада" @@ -6554,15 +6717,6 @@ "message": "Генератор", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Шта желите да генеришете?" - }, - "passwordType": { - "message": "Тип лозинке" - }, - "regenerateUsername": { - "message": "Поново генериши име" - }, "generateUsername": { "message": "Генериши име" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Тип имена" - }, "plusAddressedEmail": { "message": "Плус имејл адресе", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Случајно", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Име домаћина", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Приступни АПИ токен" - }, "deviceVerification": { "message": "Провера уређаја" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Нема колекције" }, - "canView": { - "message": "Може прегледати" - }, - "canViewExceptPass": { - "message": "Може видетим осим лозинке" - }, - "canEdit": { - "message": "Може уређивати" - }, - "canEditExceptPass": { - "message": "Ноже уређивати осим лозинке" - }, "noCollectionsAdded": { "message": "Ниједна колекција додата" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Поуздани уређаји" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Када се аутентификују, чланови ће дешифровати податке из сефљ користећи кључ сачуван на њиховом уређају", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "јединствена организација", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "полиса,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO потребан", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "полиса, и", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "администрација опоравка налога", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "полисе са аутоматским уписом ће се укључити када се користи ова опција.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Дозволе за вашу организацију су ажуриране, што захтева да поставите главну лозинку.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Ограничите брисање збирке на власнике и администраторе" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Власници и администратори могу да управљају свим колекцијама и ставкама" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Домен алијаса" - }, "alreadyHaveAccount": { "message": "Већ имате налог?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "Немате приступ за управљање овом колекцијом." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Фале дозволе „Може да управља“" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Додатје дозволе „Може да управља“ да би се омогућило потпуно управљање наплатом укључујући брисање колекције." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Одобрите групама или члановима приступ овој колекцији." @@ -9096,19 +9235,19 @@ "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." }, "eventManagement": { - "message": "Event management" + "message": "Управљање догађајима" }, "eventManagementDesc": { "message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform." }, "deviceManagement": { - "message": "Device management" + "message": "Управљање уређајима" }, "deviceManagementDesc": { - "message": "Configure device management for Bitwarden using the implementation guide for your platform." + "message": "Конфигуришите управљање уређајима за Bitwarden помоћу водича за имплементацију за своју платформу." }, "integrationCardTooltip": { - "message": "Launch $INTEGRATION$ implementation guide.", + "message": "Покренути $INTEGRATION$ водич за имплементацију.", "placeholders": { "integration": { "content": "$1", @@ -9117,7 +9256,7 @@ } }, "smIntegrationTooltip": { - "message": "Set up $INTEGRATION$.", + "message": "Подесити $INTEGRATION$.", "placeholders": { "integration": { "content": "$1", @@ -9126,7 +9265,7 @@ } }, "smSdkTooltip": { - "message": "View $SDK$ repository", + "message": "Преглед $SDK$ спремишта", "placeholders": { "sdk": { "content": "$1", @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "месечно по члану" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Места" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Алгоритам кључа" }, + "sshPrivateKey": { + "message": "Приватни кључ" + }, + "sshPublicKey": { + "message": "Јавни кључ" + }, + "sshFingerprint": { + "message": "Отисак" + }, "sshKeyFingerprint": { "message": "Отисак прста" }, @@ -9766,7 +9917,7 @@ "message": "Ваша комплементарна једногодишња претплата на Менаџер Лозинки ће надоградити на изабрани план. Неће вам бити наплаћено док се бесплатни период не заврши." }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "Датотека је сачувана на уређају. Управљајте преузимањима са свог уређаја." }, "publicApi": { "message": "Јавни API", @@ -9944,7 +10095,16 @@ "message": "We have made a micro-deposit to your bank account (this may take 1-2 business days). Enter the six-digit code starting with 'SM' found on the deposit description. Failure to verify the bank account will result in a missed payment and your subscription being suspended." }, "descriptorCode": { - "message": "Descriptor code" + "message": "Кôд дескриптора" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } }, "importantNotice": { "message": "Важно обавештење" @@ -9986,13 +10146,13 @@ "message": "Уклони чланове" }, "devices": { - "message": "Devices" + "message": "Уређаји" }, "deviceListDescription": { - "message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now." + "message": "Ваш рачун је пријављен на сваку од доле наведених уређаја. Ако не препознајете уређај, извадите га сада." }, "deviceListDescriptionTemp": { - "message": "Your account was logged in to each of the devices below." + "message": "Ваш рачун је пријављен на сваку од доле наведених уређаја." }, "claimedDomains": { "message": "Claimed domains" @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10126,15 +10286,64 @@ "message": "Organization subscription restarted" }, "restartSubscription": { - "message": "Restart your subscription" + "message": "Поново покрените претплату" }, "suspendedManagedOrgMessage": { - "message": "Contact $PROVIDER$ for assistance.", + "message": "Контактирајте $PROVIDER$ за помоћ.", "placeholders": { "provider": { "content": "$1", "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 06572d08fea8..d5971d50c106 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Kog je tipa ovaj unos?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Urеdi fasciklu" }, + "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Osnovni domen", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "npr.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Pošalji" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regeneriši lozinku" - }, "length": { "message": "Dužina" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Opasna zona" }, - "dangerZoneDesc": { - "message": "Pažljivo, ove odluke se ne mogu poništiti!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Upravljaj" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index f97376436316..b7762e4c4dcb 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Vilken typ av objekt är detta?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Redigera mapp" }, + "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Basdomän", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Objektnamn" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "t.ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Inloggning påbörjad" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Skicka" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "En avisering har skickats till din enhet." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Undvik tvetydiga tecken", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Återskapa lösenord" - }, "length": { "message": "Längd" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Farozon" }, - "dangerZoneDesc": { - "message": "Var försiktig, dessa åtgärder går inte att ångra!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Avauktorisera sessioner" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Om du fortsätter kommer du loggas ut från sin nuvarande session vilket kräver att du loggar in igen. Du kommer även behöva verifiera med tvåstegsverifiering om det är aktiverat. Aktiva sessioner på andra enheter kan fortsätta vara aktiva i upp till en timme." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Alla sessioner avauktoriserades" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Hantera" }, - "canManage": { - "message": "Kan hantera" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Stäng av" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Det gick inte att läsa säkerhetsnyckeln. Försök igen." }, - "twoFactorWebAuthnWarning": { - "message": "På grund av plattformsbegränsningar kan WebAuthn inte användas i alla Bitwarden-applikationer. Du bör aktivera en annan metod för tvåstegsverifiering så att du kan komma åt ditt konto när WebAuthn inte kan användas. Plattformar som stöds:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Webbvalvet och webbläsartillägg på en stationär eller bärbar dator med en WebAuthn-aktiverad webbläsare (Chrome, Opera, Vivaldi eller Firefox med FIDO U2F aktiverat)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Din återställningskod för tvåstegsverifiering" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "Denna användare använder tvåstegsverifiering för att skydda sitt konto." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Olänkad SSO för användare $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Skapa konto på" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Ange minimikrav för lösenordsgeneratorn." }, - "passwordGeneratorPolicyInEffect": { - "message": "En eller flera organisationspolicyer påverkar dina generatorinställningar." - }, "masterPasswordPolicyInEffect": { "message": "En eller flera organisationspolicyer kräver att ditt huvudlösenord uppfyller följande krav:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Vad skulle du vilja generera?" - }, - "passwordType": { - "message": "Lösenordstyp" - }, - "regenerateUsername": { - "message": "Återskapa användarnamn" - }, "generateUsername": { "message": "Generera användarnamn" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Användarnamnstyp" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Slumpmässigt", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Värdnamn", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API-åtkomsttoken" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Ingen samling" }, - "canView": { - "message": "Kan visa" - }, - "canViewExceptPass": { - "message": "Kan visa, utom lösenord" - }, - "canEdit": { - "message": "Kan redigera" - }, - "canEditExceptPass": { - "message": "Kan redigera, utom lösenord" - }, "noCollectionsAdded": { "message": "Ingen samlingar lades till" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Betrodda enheter" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Ägare och administratörer kan hantera alla samlingar och objekt" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Aliasdomän" - }, "alreadyHaveAccount": { "message": "Har du redan ett konto?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Ge grupper eller medlemmar tillgång till denna samling." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "månad per medlem" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Nyckelalgoritm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingeravtryck" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index e6cce3405d52..7e6f614b9c8b 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 5f5818581f63..e906b3abfd1d 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "โดเมนพื้นฐาน", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "เช่น", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "ส่ง" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Regenerate password" - }, "length": { "message": "Length" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Generator", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias domain" - }, "alreadyHaveAccount": { "message": "Already have an account?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 97c0d00dec2b..b9db895499f6 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Riskli üyeler" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Riskli uygulamalar ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Toplam üye" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Toplam uygulama" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Bu kaydın türü nedir?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Bu klasörü kalıcı olarak silmek istediğinizden emin misiniz?" + }, "baseDomain": { "message": "Ana alan adı", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Kayıt adı" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "örn.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Kimliğinizi doğrulayın" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanıyamadık. Kimliğinizi doğrulamak için e-postanıza gönderilen kodu girin." + }, + "continueLoggingIn": { + "message": "Giriş yapmaya devam et" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Giriş başlatıldı" }, + "logInRequestSent": { + "message": "İstek gönderildi" + }, "submit": { "message": "Gönder" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Cihazınıza bir bildirim gönderildi." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Hesabınıza erişmeye mi çalışıyorsunuz?" + }, + "accessAttemptBy": { + "message": "$EMAIL$ erişim denemesi", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Erişimi onayla" + }, + "denyAccess": { + "message": "Erişimi reddet" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Cihazınıza bir bildirim gönderildi" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Lütfen hesabınızın kilidinin açık olduğundan ve parmak izi ifadesinin diğer cihazla eşleştiğinden emin olun" - }, "versionNumber": { "message": "Sürüm $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Okurken karışabilecek karakterleri kullanma", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Yeni parola oluştur" - }, "length": { "message": "Uzunluk" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Tehlikeli bölge" }, - "dangerZoneDesc": { - "message": "Dikkatli olun, bu işlemleri geri alamazsınız!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Oturumları kapat" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Devam ederseniz geçerli oturumunuz da sonlanacak ve yeniden oturum açmanız gerekecek. İki aşamalı girişi etkinleştirdiyseniz onu da tamamlamanız gerekecek. Diğer cihazlardaki aktif oturumlar bir saate kadar aktif kalabilir." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Tüm oturumlar kapatıldı" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Yönet" }, - "canManage": { - "message": "Yönetebilir" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Devre dışı bırak" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Güvenlik anahtarını okurken bir sorun oluştu. Tekrar deneyin." }, - "twoFactorWebAuthnWarning": { - "message": "Platform sınırlamaları nedeniyle WebAuthn tüm Bitwarden uygulamalarında kullanılamaz. WebAuthn kullanılamadığında hesabınıza erişebilmek için başka bir iki aşamalı doğrulama yöntemi ayarlamanız gerekir. Desteklenen platformlar:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "WebAuthn uyumlu bir tarayıcıya (FIDO U2F uyumlu Chrome, Opera, Vivaldi veya Firefox) sahip bir bilgisayardaki tarayıcı uzantıları." + "twoFactorWebAuthnWarning1": { + "message": "Platform sınırlamaları nedeniyle WebAuthn tüm Bitwarden uygulamalarında kullanılamaz. WebAuthn kullanılamadığında hesabınıza erişebilmek için başka bir iki aşamalı doğrulama yöntemi ayarlamanız gerekir." }, "twoFactorRecoveryYourCode": { "message": "Bitwarden iki aşamalı giriş kurtarma kodunuz" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "1 davetiyeniz kaldı." }, + "inviteZeroEmailDesc": { + "message": "0 davetiyeniz kaldı." + }, "userUsingTwoStep": { "message": "Bu kullanıcı hesabını korumak için iki aşamalı giriş kullanıyor." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "SSO bağlantısı kesildi." + }, "unlinkedSsoUser": { "message": "$ID$ kullanıcısı için SSO bağlantısı kesildi.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Onay gerekiyor" + }, + "areYouTryingtoLogin": { + "message": "Giriş yapmaya mı çalışıyorsunuz?" + }, + "logInAttemptBy": { + "message": "$EMAIL$ giriş denemesi", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Cihaz türü" + }, + "ipAddress": { + "message": "IP adresi" + }, + "confirmLogIn": { + "message": "Girişi onayla" + }, + "denyLogIn": { + "message": "Girişi reddet" + }, + "thisRequestIsNoLongerValid": { + "message": "Bu istek artık geçerli değil." + }, + "logInConfirmedForEmailOnDevice": { + "message": "$DEVICE$ cihazında $EMAIL$ girişi onaylandı", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Başka bir cihazdan giriş isteğini reddettiniz. Yanlışlıkla yaptıysanız aynı cihazdan yeniden giriş yapmayı deneyin." + }, + "loginRequestHasAlreadyExpired": { + "message": "Giriş isteğinin süresi doldu." + }, + "justNow": { + "message": "Az önce" + }, + "requestedXMinutesAgo": { + "message": "$MINUTES$ dakika önce istendi", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Hesap oluşturuluyor:" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Parola üreticisi gereksinimleri ayarlayın." }, - "passwordGeneratorPolicyInEffect": { - "message": "Bir ya da daha fazla kuruluş ilkesi, oluşturucu ayarlarınızı etkiliyor." - }, "masterPasswordPolicyInEffect": { "message": "Bir veya daha fazla kuruluş ilkesi gereğince ana parolanız aşağıdaki gereksinimleri karşılamalıdır:" }, @@ -6554,15 +6717,6 @@ "message": "Oluşturucu", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Ne oluşturmak istersiniz?" - }, - "passwordType": { - "message": "Parola türü" - }, - "regenerateUsername": { - "message": "Kullanıcı adını yeniden oluştur" - }, "generateUsername": { "message": "Kullanıcı adı oluştur" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Kullanıcı adı türü" - }, "plusAddressedEmail": { "message": "Artı adresli e-posta", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Alan adınızın tüm iletileri yakalamaya ayarlanmış adresini kullanın." }, + "useThisEmail": { + "message": "Bu e-postayı kullan" + }, "random": { "message": "Rastgele", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Sunucu", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API erişim token'ı" - }, "deviceVerification": { "message": "Cihaz doğrulama" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Koleksiyon yok" }, - "canView": { - "message": "Görüntüleyebilir" - }, - "canViewExceptPass": { - "message": "Parolalar hariç görüntüleyebilir" - }, - "canEdit": { - "message": "Düzenleyebilir" - }, - "canEditExceptPass": { - "message": "Parolalar hariç düzenleyebilir" - }, "noCollectionsAdded": { "message": "Hiç koleksiyon eklenmedi" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Güvenilen cihazlar" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Üyeler kimliklerini doğruladıktan sonra, cihazlarında saklanan anahtarı kullanarak kasa verilerinin şifresini çözebilecektir. Bu seçeneği kullanırsanız", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "tek kuruluş", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "ilkesi,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO zorunlu", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "ilkesi ve", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "hesap kurtarma yönetimi", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "ilkesi otomatik kayıtla birlikte açılacaktır.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Kuruluş izinleriniz güncellendi ve bir ana parola belirlemeniz gerekiyor.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Koleksiyon silmeyi sahipler ve yöneticilerle sınırlandırın" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Sahipler ve yöneticiler tüm koleksiyonları ve öğeleri yönetebilir" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Alias alan adı" - }, "alreadyHaveAccount": { "message": "Zaten hesabınız var mı?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Gruplara veya üyelere bu koleksiyona erişim izni verin." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Özel anahtar" + }, + "sshPublicKey": { + "message": "Ortak anahtar" + }, + "sshFingerprint": { + "message": "Parmak izi" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Önemli uyarı" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index d8cd92707634..a72ecdc77f7a 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "Ризиковані учасники" }, + "atRiskMembersWithCount": { + "message": "Учасники з ризиком ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "Програми з ризиком ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "Ці учасники використовують у програмах слабкі, викриті, або повторювані паролі." + }, + "atRiskApplicationsDescription": { + "message": "Ці програми мають слабкі, викриті, або повторювані паролі." + }, + "atRiskMembersDescriptionWithApp": { + "message": "Ці учасники використовують у $APPNAME$ слабкі, викриті, або повторювані паролі.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Всього учасників" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Всього програм" }, + "unmarkAsCriticalApp": { + "message": "Зняти позначку критичної програми" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Позначку критичної програми знято" + }, "whatTypeOfItem": { "message": "Який це тип запису?" }, @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Основний домен", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Назва запису" }, - "cannotRemoveViewOnlyCollections": { - "message": "Ви не можете вилучати збірки, маючи дозвіл лише на перегляд: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "зразок", "description": "Short abbreviation for 'example'." @@ -1128,15 +1170,24 @@ "verifyIdentity": { "message": "Підтвердьте свою особу" }, + "weDontRecognizeThisDevice": { + "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." + }, + "continueLoggingIn": { + "message": "Продовжити вхід" + }, "whatIsADevice": { - "message": "What is a device?" + "message": "Що таке пристрій?" }, "aDeviceIs": { - "message": "A device is a unique installation of the Bitwarden app where you have logged in. Reinstalling, clearing app data, or clearing your cookies could result in a device appearing multiple times." + "message": "Пристрій – це унікальне встановлення програми Bitwarden, де ви увійшли в систему. Перевстановлення, очищення даних програми або очищення файлів cookie може призвести до того, що пристрій з'являтиметься кілька разів." }, "logInInitiated": { "message": "Ініційовано вхід" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Відправити" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Сповіщення було надіслано на ваш пристрій." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "Сповіщення надіслано на ваш пристрій" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Переконайтеся, що ваш обліковий запис розблоковано і фраза відбитка на іншому пристрої збігається" - }, "versionNumber": { "message": "Версія $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Уникати неоднозначних символів", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Генерувати новий" - }, "length": { "message": "Довжина" }, @@ -1722,10 +1797,10 @@ "message": "Повторно виконайте вхід." }, "currentSession": { - "message": "Current session" + "message": "Поточний сеанс" }, "requestPending": { - "message": "Request pending" + "message": "Запит в очікуванні" }, "logBackInOthersToo": { "message": "Будь ласка, повторно виконайте вхід. Якщо ви користуєтесь іншими програмами Bitwarden, також вийдіть із них, і знову увійдіть." @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Небезпечна зона" }, - "dangerZoneDesc": { - "message": "Обережно, ці дії неможливо скасувати!" - }, - "dangerZoneDescSingular": { - "message": "Обережно, цю дію неможливо скасувати!" - }, "deauthorizeSessions": { "message": "Завершити сеанси" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Продовжуючи, ви також вийдете з поточного сеансу і необхідно буде виконати вхід знову. Ви також отримаєте повторний запит двоетапної перевірки, якщо вона увімкнена. Активні сеанси на інших пристроях можуть залишатися активними протягом години." }, + "newDeviceLoginProtection": { + "message": "Вхід з нового пристрою" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Вимкнути захист входу з нового пристрою" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Увімкнути захист входу з нового пристрою" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Виконайте наведені нижче дії, щоб вимкнути надсилання підтверджень під час входу з нового пристрою." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Виконайте наведені нижче дії, щоб увімкнути надсилання підтверджень під час входу з нового пристрою." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Якщо захист входу з нового пристрою вимкнено, будь-хто може отримати доступ до вашого облікового запису з будь-якого пристрою, знаючи головний пароль. Щоб захистити свій обліковий запис і не вмикати надсилання підтверджень, налаштуйте двоетапну перевірку." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Зміни захисту входу з нового пристрою збережено" + }, "sessionsDeauthorized": { "message": "Усі сеанси завершено" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Керувати" }, - "canManage": { - "message": "Може керувати" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Вимкнути" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "Сталася проблема при читанні ключа безпеки. Спробуйте знову." }, - "twoFactorWebAuthnWarning": { - "message": "У зв'язку з обмеженнями платформи, WebAuthn не можна використовувати в усіх програмах Bitwarden. Вам слід активувати іншого провайдера двоетапної перевірки, щоб мати змогу отримати доступ до свого облікового запису, коли неможливо скористатися WebAuthn. Підтримувані платформи:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Вебсховище і розширення браузера на комп'ютерах і ноутбуках з браузерами, що мають підтримку WebAuthn (Chrome, Opera, Vivaldi, або Firefox з увімкненим FIDO U2F)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Ваш код відновлення двоетапної перевірки Bitwarden" @@ -3279,7 +3378,10 @@ } }, "inviteSingleEmailDesc": { - "message": "You have 1 invite remaining." + "message": "У вас залишилось 1 запрошення." + }, + "inviteZeroEmailDesc": { + "message": "У вас залишилося 0 запрошень." }, "userUsingTwoStep": { "message": "Цей користувач використовує двоетапну перевірку для захисту свого облікового запису." @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Від'єднаний SSO." + }, "unlinkedSsoUser": { "message": "Користувач $ID$ не має пов'язаного SSO.", "placeholders": { @@ -3781,13 +3886,74 @@ "message": "Пристрій" }, "loginStatus": { - "message": "Login status" + "message": "Стан входу в систему" }, "firstLogin": { - "message": "First login" + "message": "Перший вхід" }, "trusted": { - "message": "Trusted" + "message": "Надійний" + }, + "needsApproval": { + "message": "Потребує підтвердження" + }, + "areYouTryingtoLogin": { + "message": "Ви намагаєтесь увійти?" + }, + "logInAttemptBy": { + "message": "Спроба входу з $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Тип пристрою" + }, + "ipAddress": { + "message": "IP-адреса" + }, + "confirmLogIn": { + "message": "Підтвердити вхід" + }, + "denyLogIn": { + "message": "Заборонити вхід" + }, + "thisRequestIsNoLongerValid": { + "message": "Цей запит більше недійсний." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Підтверджено вхід для $EMAIL$ на $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте увійти з пристроєм знову." + }, + "loginRequestHasAlreadyExpired": { + "message": "Термін дії запиту на вхід завершився." + }, + "justNow": { + "message": "Щойно" + }, + "requestedXMinutesAgo": { + "message": "Запитано $MINUTES$ хвилин тому", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } }, "creatingAccountOn": { "message": "Створення облікового запису" @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Встановити вимоги для генерування пароля." }, - "passwordGeneratorPolicyInEffect": { - "message": "На параметри генератора впливають одна чи декілька політик організації." - }, "masterPasswordPolicyInEffect": { "message": "Політика однієї або декількох організацій зобов'язує дотримання таких вимог для головного пароля:" }, @@ -5680,17 +5843,17 @@ "message": "Помилка" }, "decryptionError": { - "message": "Decryption error" + "message": "Помилка розшифрування" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden не зміг розшифрувати вказані нижче елементи сховища." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Зверніться до служби підтримки клієнтів,", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "щоб уникнути втрати даних.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -6554,15 +6717,6 @@ "message": "Генератор", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "Що ви бажаєте згенерувати?" - }, - "passwordType": { - "message": "Тип пароля" - }, - "regenerateUsername": { - "message": "Повторно генерувати ім'я користувача" - }, "generateUsername": { "message": "Генерувати ім'я користувача" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Тип імені користувача" - }, "plusAddressedEmail": { "message": "Адреса е-пошти з плюсом", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Використовуйте свою скриньку вхідних Catch-All власного домену." }, + "useThisEmail": { + "message": "Використати цю е-пошту" + }, "random": { "message": "Випадково", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Ім'я вузла", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "Токен доступу до API" - }, "deviceVerification": { "message": "Перевірка пристрою" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "Немає збірки" }, - "canView": { - "message": "Може переглядати" - }, - "canViewExceptPass": { - "message": "Може переглядати, крім паролів" - }, - "canEdit": { - "message": "Може редагувати" - }, - "canEditExceptPass": { - "message": "Може редагувати, крім паролів" - }, "noCollectionsAdded": { "message": "Не додано жодної збірки" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Довірені пристрої" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Після автентифікації учасники розшифровуватимуть дані сховища з використанням ключа, збереженого на їхньому пристрої.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Учасникам не потрібен головний пароль під час входу з використанням SSO. Натомість використовується ключ шифрування, що зберігається на пристрої, роблячи цей пристрій довіреним. Перший пристрій, на якому учасник реєструється і входить до системи, буде довіреним. Нові пристрої необхідно схвалити за допомогою наявного довіреного пристрою або адміністратором.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "Політику єдиної", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "організації,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "політику обов'язкового", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "SSO та", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "політику адміністрування облікового", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "запису з автоматичним розгортанням буде увімкнено, якщо використовується ця опція.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "запису буде ввімкнено, якщо використовується цей параметр.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Оновлено дозволи вашої організації – вимагається встановлення головного пароля.", @@ -8275,16 +8414,16 @@ "message": "Схвалити запит" }, "deviceApproved": { - "message": "Device approved" + "message": "Пристрій схвалено" }, "deviceRemoved": { - "message": "Device removed" + "message": "Пристрій вилучено" }, "removeDevice": { - "message": "Remove device" + "message": "Вилучити пристрій" }, "removeDeviceConfirmation": { - "message": "Are you sure you want to remove this device?" + "message": "Ви дійсно хочете вилучити цей пристрій?" }, "noDeviceRequests": { "message": "Немає запитів з пристрою" @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Дозволити видалення збірок лише власникам та адміністраторам" }, + "limitItemDeletionDesc": { + "message": "Обмежити видалення записів для учасників, які мають дозвіл \"Може керувати\"" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Власники та адміністратори можуть керувати всіма збірками та записами" }, @@ -8544,9 +8686,6 @@ "message": "URL-адреса власного сервера", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Псевдонім домену" - }, "alreadyHaveAccount": { "message": "Вже маєте обліковий запис?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "У вас немає доступу до керування цією збіркою." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Немає дозволу \"Може керувати\"" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Надайте дозвіл \"Може керувати\" для можливості повноцінного керування збірками, включно з видаленням." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Надайте групам або учасникам доступ до цієї збірки." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "на місяць за учасника" }, + "monthPerMemberBilledAnnually": { + "message": "місяць за учасника сплачується щороку" + }, "seats": { "message": "Місця" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Алгоритм ключа" }, + "sshPrivateKey": { + "message": "Закритий ключ" + }, + "sshPublicKey": { + "message": "Відкритий ключ" + }, + "sshFingerprint": { + "message": "Цифровий відбиток" + }, "sshKeyFingerprint": { "message": "Цифровий відбиток" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Код дескриптора" }, + "cannotRemoveViewOnlyCollections": { + "message": "Ви не можете вилучати збірки, маючи дозвіл лише на перегляд: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Важлива інформація" }, @@ -9986,13 +10146,13 @@ "message": "Вилучити учасників" }, "devices": { - "message": "Devices" + "message": "Пристрої" }, "deviceListDescription": { - "message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now." + "message": "Вхід у ваш обліковий запис виконано на пристроях, зазначених нижче. Якщо ви не розпізнаєте пристрій, вилучіть його." }, "deviceListDescriptionTemp": { - "message": "Your account was logged in to each of the devices below." + "message": "Вхід у ваш обліковий запис виконано на пристроях, зазначених нижче." }, "claimedDomains": { "message": "Заявлені домени" @@ -10079,7 +10239,7 @@ "organizationNameMaxLength": { "message": "Назва організації не може перевищувати 50 символів." }, - "resellerRenewalWarning": { + "resellerRenewalWarningMsg": { "message": "Ваша передплата невдовзі поновиться. Щоб забезпечити безперебійну роботу, зверніться до $RESELLER$ для підтвердження поновлення до $RENEWAL_DATE$.", "placeholders": { "reseller": { @@ -10092,7 +10252,7 @@ } } }, - "resellerOpenInvoiceWarning": { + "resellerOpenInvoiceWarningMgs": { "message": "Рахунок за вашу передплату випущено $ISSUED_DATE$. Щоб забезпечити безперебійну роботу, зверніться до $RESELLER$ для підтвердження поновлення до $DUE_DATE$.", "placeholders": { "reseller": { @@ -10109,7 +10269,7 @@ } } }, - "resellerPastDueWarning": { + "resellerPastDueWarningMsg": { "message": "Рахунок за вашу передплату ще не сплачено. Щоб забезпечити безперебійну роботу, зверніться до $RESELLER$ для підтвердження поновлення до $GRACE_PERIOD_END$.", "placeholders": { "reseller": { @@ -10123,18 +10283,67 @@ } }, "restartOrganizationSubscription": { - "message": "Organization subscription restarted" + "message": "Передплату організації розпочато повторно" }, "restartSubscription": { - "message": "Restart your subscription" + "message": "Розпочніть повторно свою передплату" }, "suspendedManagedOrgMessage": { - "message": "Contact $PROVIDER$ for assistance.", + "message": "Звернутися до $PROVIDER$ по допомогу.", "placeholders": { "provider": { "content": "$1", "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index ff9f789f6f43..4b7ce90344a7 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "At-risk members" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "At-risk applications ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Mục này là gì?" }, @@ -425,6 +464,18 @@ "editFolder": { "message": "Chỉnh sửa thư mục" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Tên miền cơ sở", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "Tên mục" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "vd.", "description": "Short abbreviation for 'example'." @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "Xác minh danh tính của bạn" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Gửi" }, @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "Một thông báo đã được gửi đến thiết bị của bạn." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Phiên bản $VERSION_NUMBER$", "placeholders": { @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "Tạo lại mật khẩu" - }, "length": { "message": "Độ dài" }, @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "Vùng nguy hiểm" }, - "dangerZoneDesc": { - "message": "Cẩn thận, thao tác này không thể khôi phục!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Gỡ phiên" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "Sẽ đăng xuất bạn ra khỏi phiên hiện tại, sau đó cần đăng nhập lại. Bạn cũng sẽ phải đăng nhập hai bước lại nếu bạn có đăng nhập hai bước. Những phiên đăng nhập trên các thiết bị khác sẽ tiếp tục có hiệu lực lên đến 1 tiếng." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Tất cả phiên đăng nhập đã bị gỡ" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "Quản lý" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Vô hiệu hoá" @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "Unlinked SSO for user $ID$.", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "Creating account on" }, @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "Set requirements for password generator." }, - "passwordGeneratorPolicyInEffect": { - "message": "Các chính sách của tổ chức đang ảnh hưởng đến cài đặt tạo mật khẩu của bạn." - }, "masterPasswordPolicyInEffect": { "message": "One or more organization policies require your master password to meet the following requirements:" }, @@ -6554,15 +6717,6 @@ "message": "Trình tạo", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "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" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "Hostname", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API access token" - }, "deviceVerification": { "message": "Device verification" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "Trusted devices" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "single organization", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "policy,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "SSO required", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { + "memberDecryptionOptionTdeDescPart3": { "message": "policy, and", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "account recovery administration", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Your organization permissions were updated, requiring you to set a master password.", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Chủ sở hữu và quản trị viên có thể quản lý tất cả các bộ sưu tập và mục" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "Tên miền thay thế" - }, "alreadyHaveAccount": { "message": "Bạn đã có tài khoản?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index ae0b3744ec6c..afd675f5fa45 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -113,6 +113,39 @@ "atRiskMembers": { "message": "有风险的成员" }, + "atRiskMembersWithCount": { + "message": "有风险的成员 ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "有风险的应用程序 ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "这些成员正在使用弱的、暴露的或重复使用的密码登录到应用程序。" + }, + "atRiskApplicationsDescription": { + "message": "这些应用程序具有弱的、暴露的或重复使用的密码。" + }, + "atRiskMembersDescriptionWithApp": { + "message": "这些成员正在使用弱的、暴露的或重复使用的密码登录到 $APPNAME$。", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "总的成员" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "总的应用程序" }, + "unmarkAsCriticalApp": { + "message": "取消标记为关键应用程序" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "关键应用程序已成功取消标记" + }, "whatTypeOfItem": { "message": "这是什么类型的项目?" }, @@ -160,7 +199,7 @@ "message": "备注" }, "note": { - "message": "备注" + "message": "笔记" }, "customFields": { "message": "自定义字段" @@ -215,7 +254,7 @@ } }, "websiteAdded": { - "message": "网址已添加" + "message": "网站已添加" }, "addWebsite": { "message": "添加网站" @@ -425,6 +464,18 @@ "editFolder": { "message": "编辑文件夹" }, + "newFolder": { + "message": "新增文件夹" + }, + "folderName": { + "message": "文件夹名称" + }, + "folderHintText": { + "message": "通过在父文件夹名后面添加「/」来嵌套文件夹。示例:Social/Forums" + }, + "deleteFolderPermanently": { + "message": "您确定要永久删除这个文件夹吗?" + }, "baseDomain": { "message": "基础域", "description": "Domain name. Example: website.com" @@ -689,15 +740,6 @@ "itemName": { "message": "项目名称" }, - "cannotRemoveViewOnlyCollections": { - "message": "您无法删除仅具有「查看」权限的集合:$COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "例如", "description": "Short abbreviation for 'example'." @@ -816,7 +858,7 @@ "message": "筛选" }, "moveSelectedToOrg": { - "message": "移动所选项目到组织" + "message": "移动所选到组织" }, "deleteSelected": { "message": "删除所选" @@ -1128,6 +1170,12 @@ "verifyIdentity": { "message": "验证您的身份" }, + "weDontRecognizeThisDevice": { + "message": "我们无法识别这个设备。请输入发送到您电子邮箱中的代码以验证您的身份。" + }, + "continueLoggingIn": { + "message": "继续登录" + }, "whatIsADevice": { "message": "什么是设备?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "登录已发起" }, + "logInRequestSent": { + "message": "请求已发送" + }, "submit": { "message": "提交" }, @@ -1153,7 +1204,7 @@ "message": "主密码" }, "masterPassDesc": { - "message": "主密码是您访问密码库的密码。它非常重要,请您不要忘记。一旦忘记,无任何办法恢复此密码。" + "message": "主密码是用于访问您的密码库的密码。不要忘记您的主密码,这一点非常重要。一旦忘记,无任何办法恢复此密码。" }, "masterPassImportant": { "message": "主密码忘记后,将无法恢复!" @@ -1168,7 +1219,7 @@ "message": "主密码提示(可选)" }, "newMasterPassHint": { - "message": "新的主密码提示(可选)" + "message": "新主密码提示(可选)" }, "masterPassHintLabel": { "message": "主密码提示" @@ -1318,7 +1369,7 @@ "message": "没有可列出的事件。" }, "newOrganization": { - "message": "新建组织" + "message": "新增组织" }, "noOrganizationsList": { "message": "您没有加入任何组织。同一组织的用户可以安全地与其他用户共享项目。" @@ -1326,12 +1377,39 @@ "notificationSentDevice": { "message": "通知已发送到您的设备。" }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "您正在尝试访问您的账户吗?" + }, + "accessAttemptBy": { + "message": "$EMAIL$ 的访问尝试", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "确认访问" + }, + "denyAccess": { + "message": "拒绝访问" + }, + "notificationSentDeviceAnchor": { + "message": "网页 App" + }, + "notificationSentDevicePart2": { + "message": "在批准前,请确保指纹短语与下面的一致。" + }, + "notificationSentDeviceComplete": { + "message": "在您的设备上解锁 Bitwarden。在批准前,请确保指纹短语与下面的一致。" + }, "aNotificationWasSentToYourDevice": { "message": "通知已发送到您的设备" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "确保您的账户已解锁,并且指纹短语与其他设备上的相匹配。" - }, "versionNumber": { "message": "版本: $VERSION_NUMBER$", "placeholders": { @@ -1423,7 +1501,7 @@ "message": "FIDO U2F 安全钥匙" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "通行密钥" }, "webAuthnDesc": { "message": "使用您设备的生物识别或 WebAuthn 兼容的安全钥匙。" @@ -1619,9 +1697,6 @@ "message": "避免易混淆的字符", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "重新生成密码" - }, "length": { "message": "长度" }, @@ -1698,7 +1773,7 @@ "message": "继续操作将更改您的账户电子邮箱地址。这不会更改用于双重身份验证的电子邮箱地址。您可以在两步登录设置中更改它。" }, "newEmail": { - "message": "新的电子邮箱" + "message": "新电子邮箱" }, "code": { "message": "代码" @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "危险操作区" }, - "dangerZoneDesc": { - "message": "当心,这些操作无法撤销!" - }, - "dangerZoneDescSingular": { - "message": "当心,此操作无法撤销!" - }, "deauthorizeSessions": { "message": "取消会话授权" }, @@ -1809,6 +1878,27 @@ "deauthorizeSessionsWarning": { "message": "继续操作还将使您退出当前会话,并要求您重新登录。如果有设置两步登录,也需要重新验证。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, + "newDeviceLoginProtection": { + "message": "新设备登录" + }, + "turnOffNewDeviceLoginProtection": { + "message": "关闭新设备登录保护" + }, + "turnOnNewDeviceLoginProtection": { + "message": "开启新设备登录保护" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "按照以下步骤关闭 Bitwarden 在您从新设备登录时发送验证电子邮件的功能。" + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "按照以下步骤开启 Bitwarden 在您从新设备登录时发送验证电子邮件的功能。" + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "关闭新设备登录保护后,任何拥有您的主密码的人都可以从任何设备访问您的账户。要在没有验证电子邮件的情况下保护您的账户,请设置两步登录。" + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "新设备登录保护更改已保存" + }, "sessionsDeauthorized": { "message": "已取消所有会话授权" }, @@ -2110,8 +2200,20 @@ "manage": { "message": "管理" }, - "canManage": { - "message": "可以管理" + "manageCollection": { + "message": "管理集合" + }, + "viewItems": { + "message": "查看项目" + }, + "viewItemsHidePass": { + "message": "查看项目,隐藏密码" + }, + "editItems": { + "message": "编辑项目" + }, + "editItemsHidePass": { + "message": "编辑项目,隐藏密码" }, "disable": { "message": "停用" @@ -2219,7 +2321,7 @@ } }, "webAuthnkeyX": { - "message": "WebAuthn 密钥 $INDEX$", + "message": "WebAuthn 钥匙 $INDEX$", "placeholders": { "index": { "content": "$1", @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "读取安全钥匙时出现问题,请重试。" }, - "twoFactorWebAuthnWarning": { - "message": "由于平台限制,WebAuthn 不能在所有 Bitwarden 应用程序上使用。您应该启用另一个两步登录提供程序,以便在 WebAuthn 无法使用时可以访问您的账户。支持的平台有:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "桌面/笔记本电脑上支持 WebAuthn 的浏览器(启用了 FIDO U2F 的 Chrome、Opera、Vivaldi 或 Firefox)中的网页密码库和浏览器扩展。" + "twoFactorWebAuthnWarning1": { + "message": "由于平台限制,WebAuthn 不能在所有 Bitwarden 应用程序上使用。您应该启用另一个两步登录提供程序,以便在无法使用 WebAuthn 时可以访问您的账户。" }, "twoFactorRecoveryYourCode": { "message": "您的 Bitwarden 两步登录恢复代码" @@ -2398,7 +2497,7 @@ "message": "发现暴露的密码" }, "exposedPasswordsFoundReportDesc": { - "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个在已知的数据泄露事件中暴露了密码的项目。您应更改它们以使用新的密码。", + "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个在已知的数据泄露中暴露了密码的项目。您应更改它们以使用新的密码。", "placeholders": { "count": { "content": "$1", @@ -2411,7 +2510,7 @@ } }, "noExposedPasswords": { - "message": "您的密码库中没有在已知数据泄露事件中被暴露密码的项目。" + "message": "您的密码库中没有在已知数据泄露中暴露了密码的项目。" }, "checkExposedPasswords": { "message": "检查暴露的密码" @@ -2584,7 +2683,7 @@ "message": "请确保您的账户有足够的信用额度来用于此购买。如果您的账户信用额度不足,您的默认付款方式将用于补足差额。您可以从「计费」页面向您的账户添加信用额度。" }, "creditAppliedDesc": { - "message": "您账户的信用额度可用于进行消费。任何可用的信用额度将用于自动支付此账户的账单。" + "message": "您账户的信用额度可用于进行消费。任何可用的信用额度将用于自动抵扣此账户的账单。" }, "goPremium": { "message": "升级高级会员", @@ -2719,7 +2818,7 @@ "message": "任何未付费订阅都将通过您的付款方式收取费用。" }, "paymentChargedWithTrial": { - "message": "您的计划包含了 7 天的免费试用期。在试用期结束前,不会从您的付款方式中扣款。您可以随时取消。" + "message": "您的计划包含了 7 天的免费试用。在试用期结束前,不会从您的付款方式中扣款。您可以随时取消。" }, "paymentInformation": { "message": "支付信息" @@ -2831,10 +2930,10 @@ "message": "账单" }, "noUnpaidInvoices": { - "message": "没有未支付的账单。" + "message": "无未支付的账单。" }, "noPaidInvoices": { - "message": "没有已支付的账单。" + "message": "无已支付的账单。" }, "paid": { "message": "已支付", @@ -2860,7 +2959,7 @@ "description": "Noun. A refunded payment that was charged." }, "chargesStatement": { - "message": "任何费用将在您的对账单上以 $STATEMENT_NAME$ 显示。", + "message": "任何费用将以 $STATEMENT_NAME$ 出现在您的账单上。", "placeholders": { "statement_name": { "content": "$1", @@ -2878,7 +2977,7 @@ "message": "添加存储空间将会调整计费总金额,并立即通过您的付款方式进行扣款。第一笔费用将按当前计费周期的剩余时间按比例分配。" }, "storageRemoveNote": { - "message": "移除存储空间将会调整计费总金额,这笔费用将按比例返回下一笔账单费用中。" + "message": "移除存储空间将会调整计费总金额,这笔调整费用将按比例作为信用额度抵扣您的下一笔账单费用。" }, "adjustedStorage": { "message": "已调整 $AMOUNT$ GB 的存储空间。", @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "您还剩下 1 个邀请。" }, + "inviteZeroEmailDesc": { + "message": "您还剩下 0 个邀请。" + }, "userUsingTwoStep": { "message": "此用户正在使用两步登录来保护他们的账户。" }, @@ -3372,7 +3474,7 @@ "message": "更改了帐户密码" }, "enabledUpdated2fa": { - "message": "保存了两步登录" + "message": "两步登录已保存" }, "disabled2fa": { "message": "停用了两步登录" @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "取消链接 SSO。" + }, "unlinkedSsoUser": { "message": "为用户 $ID$ 取消链接 SSO。", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "信任" }, + "needsApproval": { + "message": "需要批准" + }, + "areYouTryingtoLogin": { + "message": "您正在尝试登录吗?" + }, + "logInAttemptBy": { + "message": "$EMAIL$ 的登录尝试", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "设备类型" + }, + "ipAddress": { + "message": "IP 地址" + }, + "confirmLogIn": { + "message": "确认登录" + }, + "denyLogIn": { + "message": "拒绝登录" + }, + "thisRequestIsNoLongerValid": { + "message": "此请求已失效。" + }, + "logInConfirmedForEmailOnDevice": { + "message": "已确认 $EMAIL$ 在 $DEVICE$ 上的登录", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "您拒绝了一个来自其他设备的登录尝试。若确实是您本人,请尝试再次发起设备登录。" + }, + "loginRequestHasAlreadyExpired": { + "message": "登录请求已过期。" + }, + "justNow": { + "message": "刚刚" + }, + "requestedXMinutesAgo": { + "message": "$MINUTES$ 分钟前已请求", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "创建账户至" }, @@ -4283,7 +4449,7 @@ "message": "添加用户席位将会调整计费总金额,并立即通过您的付款方式进行扣款。第一笔费用将按当前计费周期的剩余时间按比例分配。" }, "seatsRemoveNote": { - "message": "移除用户席位将会调整计费总金额,这笔费用将按比例返回下一笔账单费用中。" + "message": "移除用户席位将会调整计费总金额,这笔调整费用将按比例作为信用额度抵扣您的下一笔账单费用。" }, "adjustedSeats": { "message": "调整了 $AMOUNT$ 个用户席位。", @@ -4458,7 +4624,7 @@ "description": "ex. A strong password. Scale: Very Weak -> Weak -> Good -> Strong" }, "good": { - "message": "良好", + "message": "良", "description": "ex. A good password. Scale: Very Weak -> Weak -> Good -> Strong" }, "weak": { @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "设置密码生成器要求。" }, - "passwordGeneratorPolicyInEffect": { - "message": "一个或多个组织策略正在影响您的生成器设置。" - }, "masterPasswordPolicyInEffect": { "message": "一个或多个组织策略要求您的主密码满足以下要求:" }, @@ -4948,10 +5111,10 @@ "message": "确定移除此密码?" }, "hideEmail": { - "message": "对收件人隐藏我的电子邮箱地址。" + "message": "对接收者隐藏我的电子邮箱地址。" }, "disableThisSend": { - "message": "停用此 Send 则任何人无法访问它。", + "message": "停用此 Send 确保无人能访问它。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "allSends": { @@ -4976,7 +5139,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPasswordDontKnow": { - "message": "不知道密码?请向提供此 Send 的发件人索要密码。", + "message": "不知道密码吗?请向发送者索取访问此 Send 所需的密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendHiddenByDefault": { @@ -5017,7 +5180,7 @@ "message": "添加紧急联系人" }, "designatedEmergencyContacts": { - "message": "已指定为紧急联系人" + "message": "指定为紧急联系人" }, "noGrantedAccess": { "message": "您尚未被任何人指定为紧急联系人。" @@ -5182,14 +5345,14 @@ "message": "可以管理组织策略的组织成员豁免此策略。" }, "disableHideEmail": { - "message": "在创建或编辑 Send 时,始终向收件人显示成员的电子邮箱地址。", + "message": "创建或编辑 Send 时,始终向接收者显示成员的电子邮箱地址。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendOptionsPolicyInEffect": { "message": "以下组织策略目前正起作用:" }, "sendDisableHideEmailInEffect": { - "message": "创建或编辑 Send 时,不允许用户对收件人隐藏他们的电子邮箱地址。", + "message": "创建或编辑 Send 时,不允许用户对接收者隐藏他们的电子邮箱地址。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "modifiedPolicyId": { @@ -5300,10 +5463,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { - "message": "您想要发送的文本。" + "message": "您想在此 Send 中附加的文本。" }, "sendFileDesc": { - "message": "您想要发送的文件。" + "message": "您想在此 Send 中附加的文件。" }, "copySendLinkOnSave": { "message": "保存时复制链接到剪贴板以便分享此 Send。" @@ -5377,7 +5540,7 @@ "message": "发送请求" }, "addANote": { - "message": "添加备注" + "message": "添加笔记" }, "bitwardenSecretsManager": { "message": "Bitwarden 机密管理器" @@ -5752,7 +5915,7 @@ "message": "提供商" }, "newClientOrganization": { - "message": "新建客户组织" + "message": "新增客户组织" }, "newClientOrganizationDesc": { "message": "创建一个新的客户组织,该组织将作为提供商与你关联。您将可以访问和管理这个组织。" @@ -6036,7 +6199,7 @@ "message": "最小入站签名算法" }, "spWantAssertionsSigned": { - "message": "要求使用签名的断言" + "message": "期望已签名的断言" }, "spValidateCertificates": { "message": "验证证书" @@ -6072,7 +6235,7 @@ "message": "允许出站注销请求" }, "idpSignAuthenticationRequests": { - "message": "签名身份验证请求" + "message": "签署身份验证请求" }, "ssoSettingsSaved": { "message": "单点登录配置已保存" @@ -6099,7 +6262,7 @@ "message": "链接已失效。请让赞助方重新发送邀请。" }, "reclaimedFreePlan": { - "message": "重新认领免费计划" + "message": "重新申领免费计划" }, "redeem": { "message": "兑换" @@ -6171,7 +6334,7 @@ "message": "立即兑换" }, "recipient": { - "message": "收件人" + "message": "接收者" }, "removeSponsorship": { "message": "移除赞助" @@ -6554,15 +6717,6 @@ "message": "生成器", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "您想要生成什么?" - }, - "passwordType": { - "message": "密码类型" - }, - "regenerateUsername": { - "message": "重新生成用户名" - }, "generateUsername": { "message": "生成用户名" }, @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "用户名类型" - }, "plusAddressedEmail": { "message": "附加地址电子邮箱", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "使用您的域名配置的 Catch-all 收件箱。" }, + "useThisEmail": { + "message": "使用此电子邮箱" + }, "random": { "message": "随机", "description": "Generates domain-based username using random letters" @@ -6822,9 +6976,6 @@ "message": "主机名", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API 访问令牌" - }, "deviceVerification": { "message": "设备验证" }, @@ -7536,18 +7687,6 @@ "noCollection": { "message": "没有集合" }, - "canView": { - "message": "可以查看" - }, - "canViewExceptPass": { - "message": "除密码外,可以查看" - }, - "canEdit": { - "message": "可以编辑" - }, - "canEditExceptPass": { - "message": "除密码外,可以编辑" - }, "noCollectionsAdded": { "message": "未添加任何集合" }, @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "受信任设备" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "验证后,成员将使用存储在他们设备上的密钥解密密码库数据。使用此选项后,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "使用 SSO 登录时,成员无需主密码。主密码将被存储在设备上的加密密钥所取代,从而使该设备成为受信任设备。成员创建账户并登录的第一个设备将受到信任。新设备需要由现有受信任设备或管理员批准。", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { + "memberDecryptionOptionTdeDescLink1": { "message": "单一组织", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { + "memberDecryptionOptionTdeDescPart2": { "message": "策略,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { + "memberDecryptionOptionTdeDescLink2": { "message": "要求 SSO", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "策略以及具有自动注册的", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "策略,以及", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { + "memberDecryptionOptionTdeDescLink3": { "message": "账户恢复管理", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "策略将被开启。", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "策略将在使用此选项时开启。", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "您的组织权限已更新,要求您设置主密码。", @@ -8208,7 +8347,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "$TOTAL$ 不足", + "message": "总计 $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -8490,10 +8629,13 @@ "message": "管理组织的集合行为" }, "limitCollectionCreationDesc": { - "message": "限制为仅所有者和管理员可以创建集合" + "message": "限制为所有者和管理员可以创建集合" }, "limitCollectionDeletionDesc": { - "message": "限制为仅所有者和管理员可以删除集合" + "message": "限制为所有者和管理员可以删除集合" + }, + "limitItemDeletionDesc": { + "message": "限制为拥有「可以管理」权限的成员可以删除项目" }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "所有者和管理员可以管理所有集合和项目" @@ -8544,9 +8686,6 @@ "message": "自托管服务器 URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "别名域" - }, "alreadyHaveAccount": { "message": "已经拥有账户了吗?" }, @@ -8557,7 +8696,7 @@ "message": "跳转到内容" }, "managePermissionRequired": { - "message": "必须至少有一个成员或群组拥有「可以管理」的权限。" + "message": "必须至少有一个成员或群组拥有「可以管理」权限。" }, "typePasskey": { "message": "通行密钥" @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "您没有管理此集合的权限。" }, - "grantAddAccessCollectionWarningTitle": { - "message": "缺少「可以管理」权限" + "grantManageCollectionWarningTitle": { + "message": "缺少「管理集合」权限" }, - "grantAddAccessCollectionWarning": { - "message": "授予「可以管理」权限以允许完整的集合管理,包括删除集合。" + "grantManageCollectionWarning": { + "message": "授予「管理集合」权限以允许完整的集合管理,包括删除集合。" }, "grantCollectionAccess": { "message": "授予群组或成员对此集合的访问权限。" @@ -9086,7 +9225,7 @@ "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, "scimIntegrationDescEnd": { - "message": "(跨域身份管理系统)(使用您的身份提供程序的实施指南)以自动向 Bitwarden 配置用户和群组。", + "message": "(跨域身份管理系统)以自动向 Bitwarden 配置用户和群组(使用您的身份提供程序的实施指南)。", "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, "bwdc": { @@ -9171,7 +9310,10 @@ "message": "35% 折扣" }, "monthPerMember": { - "message": "每成员 /月" + "message": "月 /成员" + }, + "monthPerMemberBilledAnnually": { + "message": "月 /成员(按年计费)" }, "seats": { "message": "席位" @@ -9693,6 +9835,15 @@ "sshKeyAlgorithm": { "message": "密钥算法" }, + "sshPrivateKey": { + "message": "私钥" + }, + "sshPublicKey": { + "message": "公钥" + }, + "sshFingerprint": { + "message": "指纹" + }, "sshKeyFingerprint": { "message": "指纹" }, @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "描述符代码" }, + "cannotRemoveViewOnlyCollections": { + "message": "您无法删除仅具有「查看」权限的集合:$COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "重要通知" }, @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "组织名称不能超过 50 个字符。" }, - "resellerRenewalWarning": { - "message": "您的订阅即将续订。为确保服务不中断,请在 $RENEWAL_DATE$ 之前联系 $RESELLER$ 确认您的续订。", + "resellerRenewalWarningMsg": { + "message": "您的订阅即将续期。为确保服务不会中断,请在 $RENEWAL_DATE$ 之前联系 $RESELLER$ 确认您的续订。", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "您的订阅账单已于 $ISSUED_DATE$ 开具。为确保服务不中断,请在 $DUE_DATE$ 之前联系 $RESELLER$ 确认您的续订。", + "resellerOpenInvoiceWarningMgs": { + "message": "您的订阅账单已于 $ISSUED_DATE$ 开具。为确保服务不会中断,请在 $DUE_DATE$ 之前联系 $RESELLER$ 确认您的续订。", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "您的订阅账单尚未支付。为确保服务不中断,请在 $GRACE_PERIOD_END$ 之前联系 $RESELLER$ 确认您的续订。", + "resellerPastDueWarningMsg": { + "message": "您的订阅账单尚未支付。为确保服务不会中断,请在 $GRACE_PERIOD_END$ 之前联系 $RESELLER$ 确认您的续订。", "placeholders": { "reseller": { "content": "$1", @@ -10136,5 +10296,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "管理员现在可以删除已声明域名下的成员账户。" + }, + "deleteManagedUserWarningDesc": { + "message": "此操作将删除成员账户及其密码库中的所有项目。其取代了之前的「移除」操作。" + }, + "deleteManagedUserWarning": { + "message": "「删除」是一个新的操作!" + }, + "seatsRemaining": { + "message": "该组织已分配 $TOTAL$ 个席位,您还剩余 $REMAINING$ 个席位。请联系提供商管理您的订阅。", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "现有组织" + }, + "selectOrganizationProviderPortal": { + "message": "选择一个要添加到您的提供商门户的组织。" + }, + "noOrganizations": { + "message": "没有可列出的组织" + }, + "yourProviderSubscriptionCredit": { + "message": "您的提供商订阅将获得此组织订阅剩余时间内的信用额度。" + }, + "doYouWantToAddThisOrg": { + "message": "您想将该组织添加到 $PROVIDER$ 吗?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "添加了现有组织" + }, + "assignedExceedsAvailable": { + "message": "分配的席位超过可用席位。" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 002d0d55e0fc..fc1bf0b63f13 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -1,6 +1,6 @@ { "allApplications": { - "message": "All applications" + "message": "所有應用程式" }, "criticalApplications": { "message": "重要應用程式" @@ -108,11 +108,44 @@ "message": "全部密碼" }, "searchApps": { - "message": "Search applications" + "message": "搜尋應用程式" }, "atRiskMembers": { "message": "具有風險的成員" }, + "atRiskMembersWithCount": { + "message": "At-risk members ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskApplicationsWithCount": { + "message": "具有風險的應用程式 ($COUNT$)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "atRiskMembersDescription": { + "message": "These members are logging into applications with weak, exposed, or reused passwords." + }, + "atRiskApplicationsDescription": { + "message": "These applications have weak, exposed, or reused passwords." + }, + "atRiskMembersDescriptionWithApp": { + "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, @@ -122,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "這是什麼類型的項目?" }, @@ -267,7 +306,7 @@ "message": "安全碼 (CVV)" }, "securityCodeSlashCVV": { - "message": "Security code / CVV" + "message": "安全碼 / CVV" }, "identityName": { "message": "身分名稱" @@ -425,6 +464,18 @@ "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" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "基底網域", "description": "Domain name. Example: website.com" @@ -572,7 +623,7 @@ "message": "安全筆記" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH 金鑰" }, "typeLoginPlural": { "message": "登入資料" @@ -605,7 +656,7 @@ "message": "全名" }, "address": { - "message": "Address" + "message": "地址" }, "address1": { "message": "地址 1" @@ -689,15 +740,6 @@ "itemName": { "message": "項目名" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "例如", "description": "Short abbreviation for 'example'." @@ -722,7 +764,7 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "複製成功" }, "copyValue": { "message": "複製值", @@ -737,7 +779,7 @@ "description": "Copy passphrase to clipboard" }, "passwordCopied": { - "message": "Password copied" + "message": "已複製密碼" }, "copyUsername": { "message": "複製使用者名稱", @@ -765,28 +807,28 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "複製網站" }, "copyNotes": { - "message": "Copy notes" + "message": "複製備註" }, "copyAddress": { - "message": "Copy address" + "message": "複製地址" }, "copyPhone": { - "message": "Copy phone" + "message": "複製電話" }, "copyEmail": { "message": "複製電子郵件地址" }, "copyCompany": { - "message": "Copy company" + "message": "複製公司名稱" }, "copySSN": { - "message": "Copy Social Security number" + "message": "複製社會安全號碼" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "複製護照號碼" }, "copyLicenseNumber": { "message": "Copy license number" @@ -1099,13 +1141,13 @@ "message": "建立帳戶" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "第一次使用 Bitwarden?" }, "setAStrongPassword": { "message": "設定一個強密碼" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "設定密碼以完成建立您的帳號。" }, "newAroundHere": { "message": "第一次使用?" @@ -1117,17 +1159,23 @@ "message": "登入" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "登入 Bitwarden" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "驗證逾時" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "此驗證工作階段已逾時。請重試登入。" }, "verifyIdentity": { "message": "核實你的身份" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1137,6 +1185,9 @@ "logInInitiated": { "message": "登入已發起" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "送出" }, @@ -1174,7 +1225,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", @@ -1193,10 +1244,10 @@ "message": "Account email" }, "requestHint": { - "message": "Request hint" + "message": "請求提示" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "請求密碼提示" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "message": "Enter your account email address and your password hint will be sent to you" @@ -1239,10 +1290,10 @@ "message": "帳戶已建立!現在可以登入了。" }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "您已成功建立新帳號!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "您已經登入!" }, "trialAccountCreated": { "message": "帳戶建立成功。" @@ -1260,10 +1311,10 @@ "message": "電子郵件地址" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "您的密碼庫已被鎖定" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "您的帳號已被鎖定。" }, "uuid": { "message": "UUID" @@ -1300,7 +1351,7 @@ "message": "您沒有檢視此集合中所有項目的權限。" }, "youDoNotHavePermissions": { - "message": "You do not have permissions to this collection" + "message": "您沒有存取此集合的權限" }, "noCollectionsInList": { "message": "沒有可列出的集合。" @@ -1326,11 +1377,38 @@ "notificationSentDevice": { "message": "通知已傳送至您的裝置。" }, - "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, + "aNotificationWasSentToYourDevice": { + "message": "已傳送通知至您的裝置" }, "versionNumber": { "message": "版本 $VERSION_NUMBER$", @@ -1403,7 +1481,7 @@ "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 系列、5 系列以及 NEO 裝置。" @@ -1619,9 +1697,6 @@ "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." }, - "regeneratePassword": { - "message": "重新產生密碼" - }, "length": { "message": "長度" }, @@ -1676,10 +1751,10 @@ "message": "沒有可列出的密碼。" }, "clearHistory": { - "message": "Clear history" + "message": "清除歷史紀錄" }, "nothingToShow": { - "message": "Nothing to show" + "message": "沒有可顯示的內容" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -1722,7 +1797,7 @@ "message": "請重新登入。" }, "currentSession": { - "message": "Current session" + "message": "目前工作階段" }, "requestPending": { "message": "Request pending" @@ -1794,12 +1869,6 @@ "dangerZone": { "message": "危險區域" }, - "dangerZoneDesc": { - "message": "小心,這些動作無法復原!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "取消工作階段授權" }, @@ -1809,11 +1878,32 @@ "deauthorizeSessionsWarning": { "message": "接下來會登出目前的工作階段,並要求您重新登入。若您有設定兩步驟登入,也需重新驗證。其他裝置上的活動工作階段最多會保持一個小時。" }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "已取消所有工作階段授權" }, "accountIsOwnedMessage": { - "message": "This account is owned by $ORGANIZATIONNAME$", + "message": "此帳號由 $ORGANIZATIONNAME$ 擁有", "placeholders": { "organizationName": { "content": "$1", @@ -2110,8 +2200,20 @@ "manage": { "message": "管理" }, - "canManage": { - "message": "可以管理" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "停用" @@ -2126,19 +2228,19 @@ "message": "輸入您的主密碼以修改兩步驟登入設定。" }, "twoStepAuthenticatorInstructionPrefix": { - "message": "Download an authenticator app such as" + "message": "下載驗證應用程式,例如" }, "twoStepAuthenticatorInstructionInfix1": { - "message": "," + "message": "、" }, "twoStepAuthenticatorInstructionInfix2": { - "message": "or" + "message": "或" }, "twoStepAuthenticatorInstructionSuffix": { - "message": "." + "message": "。" }, "continueToExternalUrlTitle": { - "message": "Continue to $URL$?", + "message": "繼續前往 $URL$ 嗎?", "placeholders": { "url": { "content": "$1", @@ -2147,16 +2249,16 @@ } }, "continueToExternalUrlDesc": { - "message": "You are leaving Bitwarden and launching an external website in a new window." + "message": "您將離開 Bitwarden 並在新視窗開啟外部網站。" }, "twoStepContinueToBitwardenUrlTitle": { - "message": "Continue to bitwarden.com?" + "message": "繼續前往 bitwarden.com?" }, "twoStepContinueToBitwardenUrlDesc": { "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." }, "twoStepAuthenticatorScanCodeV2": { - "message": "Scan the QR code below with your authenticator app or enter the key." + "message": "使用驗證應用程式掃描下方的 QR 碼,或輸入金鑰。" }, "twoStepAuthenticatorQRCanvasError": { "message": "Could not load QR code. Try again or use the key below." @@ -2308,11 +2410,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "讀取安全鑰匙時發生問題。請再試一次。" }, - "twoFactorWebAuthnWarning": { - "message": "由於平台限制,無法於所有 Bitwarden 應用程式中使用 WebAuthn。請設定另一套兩步驟登入方式,以確保在 WebAuthn 無法使用時還能存取您的帳戶。支援的平台有:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "桌上型電腦/筆記型電腦上支援 WebAuthn 功能的瀏覽器(啟用了 FIDO U2F 的 Chrome、Opera、Vivaldi 或 Firefox)上的網頁版密碼庫和瀏覽器擴充套件。" + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "您的 Bitwarden 兩步驟登入復原碼" @@ -3281,6 +3380,9 @@ "inviteSingleEmailDesc": { "message": "You have 1 invite remaining." }, + "inviteZeroEmailDesc": { + "message": "You have 0 invites remaining." + }, "userUsingTwoStep": { "message": "此使用者正在使用兩步驟登入保護帳戶。" }, @@ -3732,6 +3834,9 @@ } } }, + "unlinkedSso": { + "message": "Unlinked SSO." + }, "unlinkedSsoUser": { "message": "已為使用者 $ID$ 取消連結 SSO。", "placeholders": { @@ -3789,6 +3894,67 @@ "trusted": { "message": "Trusted" }, + "needsApproval": { + "message": "Needs approval" + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "loginRequestHasAlreadyExpired": { + "message": "登入要求已逾期。" + }, + "justNow": { + "message": "剛剛" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, "creatingAccountOn": { "message": "建立帳號於" }, @@ -3913,7 +4079,7 @@ "message": "未支援您使用的瀏覽器。網頁版密碼庫可能無法正常運作。" }, "freeTrialEndPromptCount": { - "message": "Your free trial ends in $COUNT$ days.", + "message": "您的免費試用將於 $COUNT$ 天後結束。", "placeholders": { "count": { "content": "$1", @@ -3922,7 +4088,7 @@ } }, "freeTrialEndPromptMultipleDays": { - "message": "$ORGANIZATION$, your free trial ends in $COUNT$ days.", + "message": "$ORGANIZATION$,您的免費試用將於 $COUNT$ 天後結束。", "placeholders": { "count": { "content": "$2", @@ -3935,7 +4101,7 @@ } }, "freeTrialEndPromptTomorrow": { - "message": "$ORGANIZATION$, your free trial ends tomorrow.", + "message": "$ORGANIZATION$,您的免費試用明天就結束了。", "placeholders": { "organization": { "content": "$1", @@ -3944,10 +4110,10 @@ } }, "freeTrialEndPromptTomorrowNoOrgName": { - "message": "Your free trial ends tomorrow." + "message": "您的免費試用明天就結束了。" }, "freeTrialEndPromptToday": { - "message": "$ORGANIZATION$, your free trial ends today.", + "message": "$ORGANIZATION$,您的免費試用今天就結束了。", "placeholders": { "organization": { "content": "$1", @@ -3956,16 +4122,16 @@ } }, "freeTrialEndingTodayWithoutOrgName": { - "message": "Your free trial ends today." + "message": "您的免費試用今天就結束了。" }, "clickHereToAddPaymentMethod": { - "message": "Click here to add a payment method." + "message": "按一下此處來新增付款方式。" }, "joinOrganization": { "message": "加入組織" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "加入 $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -4025,7 +4191,7 @@ "message": "您已要求刪除您的 Bitwarden 帳戶。請點選下方的按鈕確認。" }, "deleteRecoverOrgConfirmDesc": { - "message": "You have requested to delete your Bitwarden organization." + "message": "您已要求刪除您的 Bitwarden 組織。" }, "myOrganization": { "message": "我的組織" @@ -4445,7 +4611,7 @@ "message": "已選擇" }, "recommended": { - "message": "Recommended" + "message": "建議" }, "ownership": { "message": "擁有權" @@ -4583,9 +4749,6 @@ "passwordGeneratorPolicyDesc": { "message": "設定密碼產生器要求。" }, - "passwordGeneratorPolicyInEffect": { - "message": "一個或多個組織原則正影響密碼產生器設定。" - }, "masterPasswordPolicyInEffect": { "message": "一個或多個組織原則要求您的主密碼須符合下列條件:" }, @@ -4764,10 +4927,10 @@ "message": "你已成功登入" }, "thisWindowWillCloseIn5Seconds": { - "message": "This window will automatically close in 5 seconds" + "message": "此視窗將在 5 秒後自動關閉。" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "您可以關閉此視窗" }, "includeAllTeamsFeatures": { "message": "包含所有團隊版功能" @@ -4932,7 +5095,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copyLink": { - "message": "Copy link" + "message": "複製連結" }, "copySendLink": { "message": "複製 Send 連結", @@ -5359,19 +5522,19 @@ "message": "Protect secrets with end-to-end encryption. No more hard coding secrets or sharing through .env files." }, "enhanceDeveloperProductivity": { - "message": "Enhance developer productivity." + "message": "提升開發者生產力。" }, "enhanceDeveloperProductivityDescription": { "message": "Programmatically retrieve and deploy secrets at runtime so developers can focus on what matters most, like improving code quality." }, "strengthenBusinessSecurity": { - "message": "Strengthen business security." + "message": "強化企業安全。" }, "strengthenBusinessSecurityDescription": { "message": "Maintain tight control over machine and human access to secrets with SSO integrations, event logs, and access rotation." }, "tryItNow": { - "message": "Try it now" + "message": "立即體驗" }, "sendRequest": { "message": "Send request" @@ -5383,7 +5546,7 @@ "message": "Bitwarden Secrets Manager" }, "moreProductsFromBitwarden": { - "message": "More products from Bitwarden" + "message": "更多來自 Bitwarden 的產品" }, "requestAccessToSecretsManager": { "message": "Request access to Secrets Manager" @@ -6554,15 +6717,6 @@ "message": "產生器", "description": "Short for 'credential generator'." }, - "whatWouldYouLikeToGenerate": { - "message": "您想要產生什麼?" - }, - "passwordType": { - "message": "密碼類型" - }, - "regenerateUsername": { - "message": "重新產生使用者名稱" - }, "generateUsername": { "message": "產生使用者名稱" }, @@ -6570,7 +6724,7 @@ "message": "Generate email" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "值必須介於 $MIN$ 及 $MAX$。", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -6603,9 +6757,6 @@ } } }, - "usernameType": { - "message": "使用者名稱類型" - }, "plusAddressedEmail": { "message": "加號地址電子郵件", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -6619,6 +6770,9 @@ "catchallEmailDesc": { "message": "使用您的網域設定的 Catch-all 收件匣。" }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "隨機", "description": "Generates domain-based username using random letters" @@ -6630,10 +6784,10 @@ "message": "Username generator" }, "useThisPassword": { - "message": "Use this password" + "message": "使用此密碼" }, "useThisUsername": { - "message": "Use this username" + "message": "使用此使用者名稱" }, "securePasswordGenerated": { "message": "Secure password generated! Don't forget to also update your password on the website." @@ -6709,7 +6863,7 @@ "message": "使用外部轉寄服務產生一個電子郵件別名。" }, "forwarderDomainName": { - "message": "Email domain", + "message": "電子信箱網域", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -6822,9 +6976,6 @@ "message": "主機名稱", "description": "Part of a URL." }, - "apiAccessToken": { - "message": "API 存取權杖" - }, "deviceVerification": { "message": "裝置驗證" }, @@ -6995,7 +7146,7 @@ "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." + "message": "啟動 Duo 並依照步驟完成登入。" }, "duoRequiredByOrgForAccount": { "message": "Duo two-step login is required for your account." @@ -7536,18 +7687,6 @@ "noCollection": { "message": "沒有分類" }, - "canView": { - "message": "可以檢視" - }, - "canViewExceptPass": { - "message": "可以檢視(除了密碼)" - }, - "canEdit": { - "message": "可以編輯" - }, - "canEditExceptPass": { - "message": "可以編輯(除了密碼)" - }, "noCollectionsAdded": { "message": "未新增任何分類" }, @@ -7852,13 +7991,13 @@ "message": "或" }, "unlockWithBiometrics": { - "message": "Unlock with biometrics" + "message": "使用生物辨識解鎖" }, "unlockWithPin": { - "message": "Unlock with PIN" + "message": "使用 PIN 碼解鎖" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "使用主密碼解鎖" }, "licenseAndBillingManagement": { "message": "授權和計費管理" @@ -8171,33 +8310,33 @@ "trustedDevices": { "message": "可信任的裝置" }, - "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "通過驗證以後,成員將使用儲存在裝置的金鑰來解密密碼庫資料。使用此選項後,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart1": { + "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "單一組織", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink1": { + "message": "single organization", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartTwo": { - "message": "原則,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart2": { + "message": "policy,", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "要求 SSO", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink2": { + "message": "SSO required", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartThree": { - "message": "原則以及具有自動注冊的", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart3": { + "message": "policy, and", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "帳戶復原管理", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescLink3": { + "message": "account recovery administration", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, - "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "原則將被開啓。", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" + "memberDecryptionOptionTdeDescPart4": { + "message": "policy will turn on when this option is used.", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { "message": "您的組織權限已更新,因此您需要設定主密碼。", @@ -8495,6 +8634,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "擁有人與管理員可以管理所有分類與項目" }, @@ -8544,9 +8686,6 @@ "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" }, - "aliasDomain": { - "message": "別名網域" - }, "alreadyHaveAccount": { "message": "已經有一個帳戶了嗎?" }, @@ -8608,11 +8747,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -8750,10 +8889,10 @@ } }, "addField": { - "message": "Add field" + "message": "新增欄位" }, "editField": { - "message": "Edit field" + "message": "編輯欄位" }, "items": { "message": "項目" @@ -9117,7 +9256,7 @@ } }, "smIntegrationTooltip": { - "message": "Set up $INTEGRATION$.", + "message": "設定 $INTEGRATION$。", "placeholders": { "integration": { "content": "$1", @@ -9126,7 +9265,7 @@ } }, "smSdkTooltip": { - "message": "View $SDK$ repository", + "message": "檢視 $SDK$ 儲存庫", "placeholders": { "sdk": { "content": "$1", @@ -9144,7 +9283,7 @@ } }, "smSdkAriaLabel": { - "message": "view $SDK$ repository in a new tab.", + "message": "在新分頁中檢視 $SDK$ 儲存庫。", "placeholders": { "sdk": { "content": "$1", @@ -9173,6 +9312,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "席位" }, @@ -9201,13 +9343,13 @@ "message": "從提供者入口網站管理計費" }, "continueSettingUpFreeTrial": { - "message": "Continue setting up your free trial of Bitwarden" + "message": "繼續設定您的 Bitwarden 免費試用" }, "continueSettingUpFreeTrialPasswordManager": { - "message": "Continue setting up your free trial of Bitwarden Password Manager" + "message": "繼續設定您的 Bitwarden 密碼管理器免費試用" }, "continueSettingUpFreeTrialSecretsManager": { - "message": "Continue setting up your free trial of Bitwarden Secrets Manager" + "message": "繼續設定您的 Bitwarden 機密管理免費試用" }, "enterTeamsOrgInfo": { "message": "Enter your Teams organization information" @@ -9461,7 +9603,7 @@ "message": "Client details" }, "downloadCSV": { - "message": "Download CSV" + "message": "下載 CSV" }, "monthlySubscriptionUserSeatsMessage": { "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " @@ -9492,10 +9634,10 @@ "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." }, "addToFolder": { - "message": "Add to folder" + "message": "新增到資料夾" }, "selectFolder": { - "message": "Select folder" + "message": "選擇資料夾" }, "personalItemTransferWarningSingular": { "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." @@ -9592,10 +9734,10 @@ "message": "Learn more about member roles and permissions" }, "whatIsACvvNumber": { - "message": "What is a CVV number?" + "message": "CVV 號碼是什麼?" }, "learnMoreAboutApi": { - "message": "Learn more about Bitwarden's API" + "message": "瞭解更多關於 Bitwarden API 的資訊" }, "fileSends": { "message": "File Sends" @@ -9693,14 +9835,23 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, "sshKeyPrivateKey": { - "message": "Private key" + "message": "私密金鑰" }, "sshKeyPublicKey": { - "message": "Public key" + "message": "公開金鑰" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -9797,7 +9948,7 @@ "message": "Enter the the field's html id, name, aria-label, or placeholder." }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "包含大寫字元", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -9805,7 +9956,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "包含小寫字元", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -9813,7 +9964,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "包含數字", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -9821,7 +9972,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "包含特殊字元", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -9829,10 +9980,10 @@ "description": "Label for the password generator special characters checkbox" }, "addAttachment": { - "message": "Add attachment" + "message": "新增附件" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "最大檔案大小為 500MB" }, "permanentlyDeleteAttachmentConfirmation": { "message": "Are you sure you want to permanently delete this attachment?" @@ -9854,7 +10005,7 @@ "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." }, "deleteOrganizationUser": { - "message": "Delete $NAME$", + "message": "刪除 $NAME$", "placeholders": { "name": { "content": "$1", @@ -9878,7 +10029,7 @@ "description": "Warning description for the bulk delete organization users dialog" }, "organizationUserDeleted": { - "message": "Deleted $NAME$", + "message": "已刪除 $NAME$", "placeholders": { "name": { "content": "$1", @@ -9929,7 +10080,7 @@ "message": "This action is not applicable to any of the selected members." }, "deletedSuccessfully": { - "message": "Deleted successfully" + "message": "刪除成功" }, "freeFamiliesSponsorship": { "message": "Remove Free Bitwarden Families sponsorship" @@ -9946,6 +10097,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -9959,7 +10119,7 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "稍後再提醒我" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -9977,10 +10137,10 @@ "message": "Yes, I can reliably access my email" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "啟動兩階段登入" }, "changeAcctEmail": { - "message": "Change account email" + "message": "更改帳號電子郵件位址" }, "removeMembers": { "message": "Remove members" @@ -10079,8 +10239,8 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, - "resellerRenewalWarning": { - "message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "resellerRenewalWarningMsg": { + "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10092,8 +10252,8 @@ } } }, - "resellerOpenInvoiceWarning": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "resellerOpenInvoiceWarningMgs": { + "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10109,8 +10269,8 @@ } } }, - "resellerPastDueWarning": { - "message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "resellerPastDueWarningMsg": { + "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10129,12 +10289,61 @@ "message": "Restart your subscription" }, "suspendedManagedOrgMessage": { - "message": "Contact $PROVIDER$ for assistance.", + "message": "聯繫 $PROVIDER$ 以取得協助。", "placeholders": { "provider": { "content": "$1", "example": "Acme c" } } + }, + "accountDeprovisioningNotification": { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/scss/pages.scss b/apps/web/src/scss/pages.scss index 684d45a1a663..1e5c233e326f 100644 --- a/apps/web/src/scss/pages.scss +++ b/apps/web/src/scss/pages.scss @@ -1,37 +1,3 @@ -app-generator { - #lengthRange { - width: 100%; - } - - .card-generated { - .card-body { - @include themify($themes) { - background: themed("foregroundColor"); - } - align-items: center; - display: flex; - flex-wrap: wrap; - font-family: $font-family-monospace; - font-size: $font-size-lg; - justify-content: center; - text-align: center; - } - } -} - -app-password-generator-history { - .list-group-item { - line-height: 1; - @include themify($themes) { - background: themed("backgroundColor"); - } - - .password { - font-family: $font-family-monospace; - } - } -} - tools-import { textarea { height: 150px; diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 2c0108ca3e2c..4a3f6cfef1b6 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -5,7 +5,7 @@ config.content = [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", - "../../libs/key-management/src/**/*.{html,ts}", + "../../libs/key-management-ui/src/**/*.{html,ts}", "../../libs/vault/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", "../../bitwarden_license/bit-web/src/**/*.{html,ts}", diff --git a/apps/web/tsconfig.build.json b/apps/web/tsconfig.build.json index b10c4f9d899c..39ab37efbb84 100644 --- a/apps/web/tsconfig.build.json +++ b/apps/web/tsconfig.build.json @@ -1,5 +1,8 @@ { "extends": "./tsconfig.json", "files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"], - "include": ["src/connectors/*.ts", "../../libs/common/src/platform/services/**/*.worker.ts"] + "include": [ + "src/connectors/*.ts", + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts" + ] } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 678db7c4af55..68ac8c800852 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -7,8 +7,8 @@ "paths": { "@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"], + "@bitwarden/auth/common": ["../../libs/auth/src/common"], "@bitwarden/billing": ["../../libs/billing/src"], "@bitwarden/bit-common/*": ["../../bitwarden_license/bit-common/src/*"], "@bitwarden/common/*": ["../../libs/common/src/*"], @@ -18,17 +18,18 @@ "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], - "@bitwarden/vault-export-core": [ - "../../libs/tools/export/vault-export/vault-export-core/src" - ], - "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], - "@bitwarden/importer/core": ["../../libs/importer/src"], - "@bitwarden/importer/ui": ["../../libs/importer/src/components"], + "@bitwarden/importer-core": ["../../libs/importer/src"], + "@bitwarden/importer-ui": ["../../libs/importer/src/components"], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], + "@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], + "@bitwarden/ui-common": ["../../libs/ui/common/src"], + "@bitwarden/vault-export-core": [ + "../../libs/tools/export/vault-export/vault-export-core/src" + ], + "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/web-vault/*": ["src/*"] } @@ -42,6 +43,6 @@ "src/connectors/*.ts", "src/**/*.stories.ts", "src/**/*.spec.ts", - "../../libs/common/src/platform/services/**/*.worker.ts" + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts" ] } diff --git a/bitwarden_license/bit-cli/.eslintrc.json b/bitwarden_license/bit-cli/.eslintrc.json deleted file mode 100644 index 10d223883786..000000000000 --- a/bitwarden_license/bit-cli/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "node": true - } -} diff --git a/bitwarden_license/bit-cli/src/admin-console/.eslintrc.json b/bitwarden_license/bit-cli/src/admin-console/.eslintrc.json deleted file mode 100644 index 38467187294d..000000000000 --- a/bitwarden_license/bit-cli/src/admin-console/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../../libs/admin-console/.eslintrc.json" -} diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts index db63a3c1c685..dc78ebf7cd03 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class ApproveAllCommand { constructor( private organizationAuthRequestService: OrganizationAuthRequestService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,13 @@ export class ApproveAllCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -58,6 +67,7 @@ export class ApproveAllCommand { return new ApproveAllCommand( serviceContainer.organizationAuthRequestService, serviceContainer.organizationService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts index 1c51f9397c5c..27597fd1a859 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts @@ -1,16 +1,19 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; export class ApproveCommand { constructor( - private organizationService: OrganizationService, + private organizationService: DefaultOrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountService: AccountService, ) {} async run(organizationId: string, id: string): Promise { @@ -30,7 +33,17 @@ export class ApproveCommand { return Response.badRequest("`" + id + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations?.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -57,6 +70,7 @@ export class ApproveCommand { return new ApproveCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts index 767acea99f78..923545f15ef5 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class DenyAllCommand { constructor( private organizationService: OrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,17 @@ export class DenyAllCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -54,6 +67,7 @@ export class DenyAllCommand { return new DenyAllCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts index 87e633b2bee4..ac5c285f8d7f 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts @@ -1,8 +1,9 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -11,6 +12,7 @@ export class DenyCommand { constructor( private organizationService: OrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountServcie: AccountService, ) {} async run(organizationId: string, id: string): Promise { @@ -30,7 +32,17 @@ export class DenyCommand { return Response.badRequest("`" + id + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(this.accountServcie.activeAccount$.pipe(map((a) => a?.id))); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -57,6 +69,7 @@ export class DenyCommand { return new DenyCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts index 972be460df75..31a0e748175d 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts @@ -1,9 +1,11 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { ListResponse } from "@bitwarden/cli/models/response/list.response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class ListCommand { constructor( private organizationAuthRequestService: OrganizationAuthRequestService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,17 @@ export class ListCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -46,6 +59,7 @@ export class ListCommand { return new ListCommand( serviceContainer.organizationAuthRequestService, serviceContainer.organizationService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/tsconfig.json b/bitwarden_license/bit-cli/tsconfig.json index 92a206f44dbf..4a972b540a71 100644 --- a/bitwarden_license/bit-cli/tsconfig.json +++ b/bitwarden_license/bit-cli/tsconfig.json @@ -18,13 +18,12 @@ "@bitwarden/auth/common": ["../../libs/auth/src/common"], "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/common/*": ["../../libs/common/src/*"], - "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer-core": ["../../libs/importer/src"], "@bitwarden/generator-core": ["../../libs/tools/generator/core/src"], "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], diff --git a/bitwarden_license/bit-common/jest.config.js b/bitwarden_license/bit-common/jest.config.js index a0441b01883d..ab31a4c26cab 100644 --- a/bitwarden_license/bit-common/jest.config.js +++ b/bitwarden_license/bit-common/jest.config.js @@ -7,9 +7,17 @@ module.exports = { ...sharedConfig, displayName: "bit-common tests", testEnvironment: "jsdom", - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + { + "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/common": ["../../libs/common/src/*"], + "@bitwarden/admin-console/common": ["/libs/admin-console/src/common"], + ...(compilerOptions?.paths ?? {}), + }, + { + prefix: "/", + }, + ), setupFilesAfterEnv: ["/test.setup.ts"], transformIgnorePatterns: ["node_modules/(?!(.*\\.mjs$|@angular|rxjs|@bitwarden))"], moduleFileExtensions: ["ts", "js", "html", "mjs"], diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts index e893b2dfe8c5..448399a8bb04 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts @@ -4,8 +4,8 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordDetailsResponse, } from "@bitwarden/admin-console/common"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { KeyService } from "@bitwarden/key-management"; diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts index 4c4507e5cb82..025b021f83d9 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts @@ -4,7 +4,7 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordDetailsResponse, } from "@bitwarden/admin-console/common"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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"; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts index 947fc8a79d39..723d737d5bd6 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts @@ -1,6 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Opaque } from "type-fest"; + +import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant } from "@bitwarden/components"; @@ -82,6 +85,7 @@ export type WeakPasswordScore = { * How many times a password has been exposed */ export type ExposedPasswordDetail = { + cipherId: string; exposedXTimes: number; } | null; @@ -113,3 +117,43 @@ export type AtRiskApplicationDetail = { applicationName: string; atRiskPasswordCount: number; }; + +export type AppAtRiskMembersDialogParams = { + members: MemberDetailsFlat[]; + applicationName: string; +}; + +/** + * Request to drop a password health report application + * Model is expected by the API endpoint + */ +export interface PasswordHealthReportApplicationDropRequest { + organizationId: OrganizationId; + passwordHealthReportApplicationIds: string[]; +} + +/** + * Response from the API after marking an app as critical + */ +export interface PasswordHealthReportApplicationsResponse { + id: PasswordHealthReportApplicationId; + organizationId: OrganizationId; + uri: string; +} +/* + * Request to save a password health report application + * Model is expected by the API endpoint + */ +export interface PasswordHealthReportApplicationsRequest { + organizationId: OrganizationId; + url: string; +} + +export enum DrawerType { + None = 0, + AppAtRiskMembers = 1, + OrgAtRiskMembers = 2, + OrgAtRiskApps = 3, +} + +export type PasswordHealthReportApplicationId = Opaque; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts index 838dc2c8241c..5ed88c4cac9e 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts @@ -3,12 +3,14 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { CriticalAppsApiService } from "./critical-apps-api.service"; import { + PasswordHealthReportApplicationDropRequest, PasswordHealthReportApplicationId, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; + +import { CriticalAppsApiService } from "./critical-apps-api.service"; describe("CriticalAppsApiService", () => { let service: CriticalAppsApiService; @@ -76,4 +78,24 @@ describe("CriticalAppsApiService", () => { done(); }); }); + + it("should call apiService.send with correct parameters for DropCriticalApp", (done) => { + const request: PasswordHealthReportApplicationDropRequest = { + organizationId: "org1" as OrganizationId, + passwordHealthReportApplicationIds: ["123"], + }; + + apiService.send.mockReturnValue(Promise.resolve()); + + service.dropCriticalApp(request).subscribe(() => { + expect(apiService.send).toHaveBeenCalledWith( + "DELETE", + "/reports/password-health-report-application/", + request, + true, + true, + ); + done(); + }); + }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts index edd2cf34b563..c02a3686dfd4 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts @@ -4,9 +4,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { + PasswordHealthReportApplicationDropRequest, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; export class CriticalAppsApiService { constructor(private apiService: ApiService) {} @@ -36,4 +37,16 @@ export class CriticalAppsApiService { return from(dbResponse as Promise); } + + dropCriticalApp(request: PasswordHealthReportApplicationDropRequest): Observable { + const dbResponse = this.apiService.send( + "DELETE", + "/reports/password-health-report-application/", + request, + true, + true, + ); + + return from(dbResponse as Promise); + } } diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts index c6c4562310e6..64f55ccf99f1 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts @@ -4,7 +4,7 @@ import { fakeAsync, flush } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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"; @@ -12,13 +12,14 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; -import { CriticalAppsApiService } from "./critical-apps-api.service"; import { - CriticalAppsService, PasswordHealthReportApplicationId, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; + +import { CriticalAppsApiService } from "./critical-apps-api.service"; +import { CriticalAppsService } from "./critical-apps.service"; describe("CriticalAppsService", () => { let service: CriticalAppsService; @@ -139,4 +140,54 @@ describe("CriticalAppsService", () => { expect(res).toHaveLength(2); }); }); + + it("should drop a critical app", async () => { + // arrange + const orgId = "org1" as OrganizationId; + const selectedUrl = "https://example.com"; + + const initialList = [ + { id: "id1", organizationId: "org1", uri: "https://example.com" }, + { id: "id2", organizationId: "org1", uri: "https://example.org" }, + ] as PasswordHealthReportApplicationsResponse[]; + + service.setAppsInListForOrg(initialList); + + // act + await service.dropCriticalApp(orgId, selectedUrl); + + // expectations + expect(criticalAppsApiService.dropCriticalApp).toHaveBeenCalledWith({ + organizationId: orgId, + passwordHealthReportApplicationIds: ["id1"], + }); + expect(service.getAppsListForOrg(orgId)).toBeTruthy(); + service.getAppsListForOrg(orgId).subscribe((res) => { + expect(res).toHaveLength(1); + expect(res[0].uri).toBe("https://example.org"); + }); + }); + + it("should not drop a critical app if it does not exist", async () => { + // arrange + const orgId = "org1" as OrganizationId; + const selectedUrl = "https://nonexistent.com"; + + const initialList = [ + { id: "id1", organizationId: "org1", uri: "https://example.com" }, + { id: "id2", organizationId: "org1", uri: "https://example.org" }, + ] as PasswordHealthReportApplicationsResponse[]; + + service.setAppsInListForOrg(initialList); + + // act + await service.dropCriticalApp(orgId, selectedUrl); + + // expectations + expect(criticalAppsApiService.dropCriticalApp).not.toHaveBeenCalled(); + expect(service.getAppsListForOrg(orgId)).toBeTruthy(); + service.getAppsListForOrg(orgId).subscribe((res) => { + expect(res).toHaveLength(2); + }); + }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts index 10b7d3f1fbbb..7db34757b962 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts @@ -12,14 +12,18 @@ import { takeUntil, zip, } from "rxjs"; -import { Opaque } from "type-fest"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { + PasswordHealthReportApplicationsRequest, + PasswordHealthReportApplicationsResponse, +} from "../models/password-health"; + import { CriticalAppsApiService } from "./critical-apps-api.service"; /* Retrieves and decrypts critical apps for a given organization @@ -94,6 +98,25 @@ export class CriticalAppsService { this.orgId.next(orgId); } + // Drop a critical app for a given organization + // Only one app may be dropped at a time + async dropCriticalApp(orgId: OrganizationId, selectedUrl: string) { + const app = this.criticalAppsList.value.find( + (f) => f.organizationId === orgId && f.uri === selectedUrl, + ); + + if (!app) { + return; + } + + await this.criticalAppsApiService.dropCriticalApp({ + organizationId: app.organizationId, + passwordHealthReportApplicationIds: [app.id], + }); + + this.criticalAppsList.next(this.criticalAppsList.value.filter((f) => f.uri !== selectedUrl)); + } + private retrieveCriticalApps( orgId: OrganizationId | null, ): Observable { @@ -144,16 +167,3 @@ export class CriticalAppsService { return await Promise.all(criticalAppsPromises); } } - -export interface PasswordHealthReportApplicationsRequest { - organizationId: OrganizationId; - url: string; -} - -export interface PasswordHealthReportApplicationsResponse { - id: PasswordHealthReportApplicationId; - organizationId: OrganizationId; - uri: string; -} - -export type PasswordHealthReportApplicationId = Opaque; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts index 42bab69fca43..386c6fd68656 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts @@ -1,10 +1,15 @@ import { BehaviorSubject } from "rxjs"; import { finalize } from "rxjs/operators"; -import { ApplicationHealthReportDetail } from "../models/password-health"; +import { + AppAtRiskMembersDialogParams, + ApplicationHealthReportDetail, + AtRiskApplicationDetail, + AtRiskMemberDetail, + DrawerType, +} from "../models/password-health"; import { RiskInsightsReportService } from "./risk-insights-report.service"; - export class RiskInsightsDataService { private applicationsSubject = new BehaviorSubject(null); @@ -22,6 +27,13 @@ export class RiskInsightsDataService { private dataLastUpdatedSubject = new BehaviorSubject(null); dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); + openDrawer = false; + drawerInvokerId: string = ""; + activeDrawerType: DrawerType = DrawerType.None; + atRiskMemberDetails: AtRiskMemberDetail[] = []; + appAtRiskMembers: AppAtRiskMembersDialogParams | null = null; + atRiskAppDetails: AtRiskApplicationDetail[] | null = null; + constructor(private reportService: RiskInsightsReportService) {} /** @@ -57,4 +69,57 @@ export class RiskInsightsDataService { refreshApplicationsReport(organizationId: string): void { this.fetchApplicationsReport(organizationId, true); } + + isActiveDrawerType = (drawerType: DrawerType): boolean => { + return this.activeDrawerType === drawerType; + }; + + setDrawerForOrgAtRiskMembers = ( + atRiskMemberDetails: AtRiskMemberDetail[], + invokerId: string = "", + ): void => { + this.resetDrawer(DrawerType.OrgAtRiskMembers); + this.activeDrawerType = DrawerType.OrgAtRiskMembers; + this.drawerInvokerId = invokerId; + this.atRiskMemberDetails = atRiskMemberDetails; + this.openDrawer = !this.openDrawer; + }; + + setDrawerForAppAtRiskMembers = ( + atRiskMembersDialogParams: AppAtRiskMembersDialogParams, + invokerId: string = "", + ): void => { + this.resetDrawer(DrawerType.None); + this.activeDrawerType = DrawerType.AppAtRiskMembers; + this.drawerInvokerId = invokerId; + this.appAtRiskMembers = atRiskMembersDialogParams; + this.openDrawer = !this.openDrawer; + }; + + setDrawerForOrgAtRiskApps = ( + atRiskApps: AtRiskApplicationDetail[], + invokerId: string = "", + ): void => { + this.resetDrawer(DrawerType.OrgAtRiskApps); + this.activeDrawerType = DrawerType.OrgAtRiskApps; + this.drawerInvokerId = invokerId; + this.atRiskAppDetails = atRiskApps; + this.openDrawer = !this.openDrawer; + }; + + closeDrawer = (): void => { + this.resetDrawer(DrawerType.None); + }; + + private resetDrawer = (drawerType: DrawerType): void => { + if (this.activeDrawerType !== drawerType) { + this.openDrawer = false; + } + + this.activeDrawerType = DrawerType.None; + this.atRiskMemberDetails = []; + this.appAtRiskMembers = null; + this.atRiskAppDetails = null; + this.drawerInvokerId = ""; + }; } diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts index c3bcc59eca50..027760f678c4 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts @@ -175,6 +175,7 @@ export class RiskInsightsReportService { ): Promise { const cipherHealthReports: CipherHealthReportDetail[] = []; const passwordUseMap = new Map(); + const exposedDetails = await this.findExposedPasswords(ciphers); for (const cipher of ciphers) { if (this.validateCipher(cipher)) { const weakPassword = this.findWeakPassword(cipher); @@ -189,7 +190,7 @@ export class RiskInsightsReportService { passwordUseMap.set(cipher.login.password, 1); } - const exposedPassword = await this.findExposedPassword(cipher); + const exposedPassword = exposedDetails.find((x) => x.cipherId === cipher.id); // Get the cipher members const cipherMembers = memberDetails.filter((x) => x.cipherId === cipher.id); @@ -255,13 +256,29 @@ export class RiskInsightsReportService { return appReports; } - private async findExposedPassword(cipher: CipherView): Promise { - const exposedCount = await this.auditService.passwordLeaked(cipher.login.password); - if (exposedCount > 0) { - const exposedDetail = { exposedXTimes: exposedCount } as ExposedPasswordDetail; - return exposedDetail; - } - return null; + private async findExposedPasswords(ciphers: CipherView[]): Promise { + const exposedDetails: ExposedPasswordDetail[] = []; + const promises: Promise[] = []; + + ciphers.forEach((ciph) => { + if (this.validateCipher(ciph)) { + const promise = this.auditService + .passwordLeaked(ciph.login.password) + .then((exposedCount) => { + if (exposedCount > 0) { + const detail = { + exposedXTimes: exposedCount, + cipherId: ciph.id, + } as ExposedPasswordDetail; + exposedDetails.push(detail); + } + }); + promises.push(promise); + } + }); + await Promise.all(promises); + + return exposedDetails; } private findWeakPassword(cipher: CipherView): WeakPasswordDetail { diff --git a/bitwarden_license/bit-common/tsconfig.json b/bitwarden_license/bit-common/tsconfig.json index 7791840950bb..bc36576f1b3d 100644 --- a/bitwarden_license/bit-common/tsconfig.json +++ b/bitwarden_license/bit-common/tsconfig.json @@ -9,6 +9,7 @@ "@bitwarden/angular/*": ["../../libs/angular/src/*"], "@bitwarden/auth": ["../../libs/auth/src"], "@bitwarden/billing": ["../../libs/billing/src"], + "@bitwarden/bit-common/*": ["../bit-common/src/*"], "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/components": ["../../libs/components/src"], "@bitwarden/generator-components": ["../../libs/tools/generator/components/src"], @@ -16,18 +17,17 @@ "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../../libs/key-management/src"], + "@bitwarden/platform": ["../../libs/platform/src"], + "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], + "@bitwarden/tools-card": ["../../libs/tools/card/src"], + "@bitwarden/ui-common": ["../../libs/ui/common/src"], "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"], - "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], - "@bitwarden/tools-card": ["../../libs/tools/card/src"], - "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], - "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/vault": ["../../libs/vault/src"], - "@bitwarden/web-vault/*": ["../../apps/web/src/*"], - "@bitwarden/bit-common/*": ["../bit-common/src/*"] + "@bitwarden/web-vault/*": ["../../apps/web/src/*"] } } } diff --git a/bitwarden_license/bit-web/jest.config.js b/bitwarden_license/bit-web/jest.config.js index 17b7139049a1..9c9c61b24021 100644 --- a/bitwarden_license/bit-web/jest.config.js +++ b/bitwarden_license/bit-web/jest.config.js @@ -9,7 +9,15 @@ module.exports = { ...sharedConfig, preset: "jest-preset-angular", setupFilesAfterEnv: ["../../apps/web/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + { + "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/common": ["../../libs/common/src/*"], + "@bitwarden/admin-console/common": ["/libs/admin-console/src/common"], + ...(compilerOptions?.paths ?? {}), + }, + { + prefix: "/", + }, + ), }; diff --git a/bitwarden_license/bit-web/src/app/admin-console/.eslintrc.json b/bitwarden_license/bit-web/src/app/admin-console/.eslintrc.json deleted file mode 100644 index d55df3899e75..000000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../../../libs/admin-console/.eslintrc.json" -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts index ac8ad3112b9e..744cf2c46740 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts @@ -10,8 +10,8 @@ import { OrganizationAuthRequestApiService } from "@bitwarden/bit-common/admin-c import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests/organization-auth-request.service"; import { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request.view"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html index ac83491538eb..c292d51ebdae 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html @@ -4,7 +4,11 @@ -

+

{{ "claimedDomainsDesc" | i18n }} - + {{ "newClient" | i18n }} @@ -24,63 +28,55 @@ {{ "loading" | i18n }} - -

{{ "noClientsInList" | i18n }}

- - +

{{ "noClientsInList" | i18n }}

+ + - - - - - - - - - - - - - - - + + + + + + + + + + - - -
{{ "name" | i18n }}{{ "numberOfUsers" | i18n }}{{ "billingPlan" | i18n }}
- - - {{ o.organizationName }} - - {{ o.userCount }} - / {{ o.seats }} - - {{ o.plan }} - - {{ "name" | i18n }}{{ "numberOfUsers" | i18n }}{{ "billingPlan" | i18n }} + + + {{ row.organizationName }} + + {{ row.userCount }} + / {{ row.seats }} + + {{ row.plan }} + +
+
+ + +
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 80ed30253069..f830b149db4c 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,26 +1,37 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute, Router, RouterModule } from "@angular/router"; import { firstValueFrom, from, map } from "rxjs"; -import { switchMap, takeUntil } from "rxjs/operators"; +import { debounceTime, first, switchMap } from "rxjs/operators"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.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 { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlanType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { WebProviderService } from "../services/web-provider.service"; import { AddOrganizationComponent } from "./add-organization.component"; -import { BaseClientsComponent } from "./base-clients.component"; const DisallowedPlanTypes = [ PlanType.Free, @@ -32,13 +43,26 @@ const DisallowedPlanTypes = [ @Component({ templateUrl: "clients.component.html", + standalone: true, + imports: [ + SharedOrganizationModule, + HeaderModule, + CommonModule, + JslibModule, + AvatarModule, + RouterModule, + TableModule, + ], }) -export class ClientsComponent extends BaseClientsComponent implements OnInit, OnDestroy { - providerId: string; - addableOrganizations: Organization[]; +export class ClientsComponent { + providerId: string = ""; + addableOrganizations: Organization[] = []; loading = true; manageOrganizations = false; showAddExisting = false; + dataSource: TableDataSource = + new TableDataSource(); + protected searchControl = new FormControl("", { nonNullable: true }); constructor( private router: Router, @@ -46,28 +70,20 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On private apiService: ApiService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - activatedRoute: ActivatedRoute, - dialogService: DialogService, - i18nService: I18nService, - searchService: SearchService, - toastService: ToastService, - validationService: ValidationService, - webProviderService: WebProviderService, + private accountService: AccountService, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, ) { - super( - activatedRoute, - dialogService, - i18nService, - searchService, - toastService, - validationService, - webProviderService, - ); - } + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); - ngOnInit() { - this.activatedRoute.parent.params - .pipe( + this.activatedRoute.parent?.params + ?.pipe( switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( @@ -85,23 +101,52 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On }), ); }), - takeUntil(this.destroy$), + takeUntilDestroyed(), ) .subscribe(); + + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); } - ngOnDestroy() { - super.ngOnDestroy(); + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } } async load() { const response = await this.apiService.getProviderClients(this.providerId); - this.clients = response.data != null && response.data.length > 0 ? response.data : []; + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const clients = response.data != null && response.data.length > 0 ? response.data : []; + this.dataSource.data = clients; this.manageOrganizations = (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; - const candidateOrgs = (await this.organizationService.getAll()).filter( - (o) => o.isOwner && o.providerId == null, - ); + const candidateOrgs = ( + await firstValueFrom(this.organizationService.organizations$(userId)) + ).filter((o) => o.isOwner && o.providerId == null); const allowedOrgsIds = await Promise.all( candidateOrgs.map((o) => this.organizationApiService.get(o.id)), ).then((orgs) => diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html deleted file mode 100644 index e6b5ae122f32..000000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - {{ "newClient" | i18n }} - - - - - - - {{ "loading" | i18n }} - - - -

{{ "noClientsInList" | i18n }}

- - - - {{ "name" | i18n }} - {{ "numberOfUsers" | i18n }} - {{ "billingPlan" | i18n }} - - - - - - - - {{ row.organizationName }} - - - {{ row.userCount }} - / {{ row.seats }} - - - {{ row.plan }} - - - - - - - -
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts deleted file mode 100644 index 2be38477d4c4..000000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router, RouterModule } from "@angular/router"; -import { firstValueFrom, from, map } from "rxjs"; -import { debounceTime, first, switchMap } from "rxjs/operators"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -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 { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { - AvatarModule, - DialogService, - TableDataSource, - TableModule, - ToastService, -} from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; - -import { WebProviderService } from "../services/web-provider.service"; - -import { AddOrganizationComponent } from "./add-organization.component"; - -const DisallowedPlanTypes = [ - PlanType.Free, - PlanType.FamiliesAnnually2019, - PlanType.FamiliesAnnually, - PlanType.TeamsStarter2023, - PlanType.TeamsStarter, -]; - -@Component({ - templateUrl: "vnext-clients.component.html", - standalone: true, - imports: [ - SharedOrganizationModule, - HeaderModule, - CommonModule, - JslibModule, - AvatarModule, - RouterModule, - TableModule, - ], -}) -export class vNextClientsComponent { - providerId: string = ""; - addableOrganizations: Organization[] = []; - loading = true; - manageOrganizations = false; - showAddExisting = false; - dataSource: TableDataSource = - new TableDataSource(); - protected searchControl = new FormControl("", { nonNullable: true }); - - constructor( - private router: Router, - private providerService: ProviderService, - private apiService: ApiService, - private organizationService: OrganizationService, - private organizationApiService: OrganizationApiServiceAbstraction, - private activatedRoute: ActivatedRoute, - private dialogService: DialogService, - private i18nService: I18nService, - private toastService: ToastService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - ) { - this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { - this.searchControl.setValue(queryParams.search); - }); - - this.activatedRoute.parent?.params - ?.pipe( - switchMap((params) => { - this.providerId = params.providerId; - return this.providerService.get$(this.providerId).pipe( - map((provider) => provider?.providerStatus === ProviderStatusType.Billable), - map((isBillable) => { - if (isBillable) { - return from( - this.router.navigate(["../manage-client-organizations"], { - relativeTo: this.activatedRoute, - }), - ); - } else { - return from(this.load()); - } - }), - ); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((searchText) => { - this.dataSource.filter = (data) => - data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; - }); - } - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - await this.webProviderService.detachOrganization(this.providerId, organization.id); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("detachedOrganization", organization.organizationName), - }); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - } - - async load() { - const response = await this.apiService.getProviderClients(this.providerId); - const clients = response.data != null && response.data.length > 0 ? response.data : []; - this.dataSource.data = clients; - this.manageOrganizations = - (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; - const candidateOrgs = (await this.organizationService.getAll()).filter( - (o) => o.isOwner && o.providerId == null, - ); - const allowedOrgsIds = await Promise.all( - candidateOrgs.map((o) => this.organizationApiService.get(o.id)), - ).then((orgs) => - orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id), - ); - this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id)); - - this.showAddExisting = this.addableOrganizations.length !== 0; - this.loading = false; - } - - async addExistingOrganization() { - const dialogRef = AddOrganizationComponent.open(this.dialogService, { - providerId: this.providerId, - organizations: this.addableOrganizations, - }); - - if (await firstValueFrom(dialogRef.closed)) { - await this.load(); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts index bd8c54c39d70..56584b67f02a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts @@ -2,9 +2,7 @@ // @ts-strict-ignore import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -33,9 +31,8 @@ export class AcceptProviderComponent extends BaseAcceptComponent { authService: AuthService, private apiService: ApiService, platformUtilService: PlatformUtilsService, - registerRouteService: RegisterRouteService, ) { - super(router, platformUtilService, i18nService, route, authService, registerRouteService); + super(router, platformUtilService, i18nService, route, authService); } async authedHandler(qParams: Params) { @@ -47,6 +44,7 @@ export class AcceptProviderComponent extends BaseAcceptComponent { qParams.providerUserId, request, ); + this.platformUtilService.showToast( "success", this.i18nService.t("inviteAccepted"), @@ -64,25 +62,14 @@ export class AcceptProviderComponent extends BaseAcceptComponent { } 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 = { + // We don't need users to complete email verification if they are coming directly from an emailed invite. + // Therefore, we skip /signup and navigate directly to /finish-signup. + await this.router.navigate(["/finish-signup"], { + queryParams: { email: this.email, providerUserId: this.providerUserId, providerInviteToken: this.providerInviteToken, - }; - } - - await this.router.navigate([registerRoute], { - queryParams: queryParams, + }, }); } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index 6ab07cc07942..c5b949512d77 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -13,8 +13,8 @@ import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/ import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; 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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { DialogService } from "@bitwarden/components"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index 16f794bd6d2e..03e47569a55c 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -15,8 +15,8 @@ import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admi import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index a20dd1379e20..bf82fbd160b0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -37,25 +37,5 @@ > - - {{ "providerClientVaultPrivacyNotification" | i18n }} - - {{ "contactBitwardenSupport" | i18n }} . - diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index 3f1a7ff3989f..7e47da95e2bc 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -10,27 +10,15 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { BannerModule, IconModule, LinkModule } from "@bitwarden/components"; +import { IconModule } from "@bitwarden/components"; import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo"; import { WebLayoutModule } from "@bitwarden/web-vault/app/layouts/web-layout.module"; -import { ProviderClientVaultPrivacyBannerService } from "./services/provider-client-vault-privacy-banner.service"; - @Component({ selector: "providers-layout", templateUrl: "providers-layout.component.html", standalone: true, - imports: [ - CommonModule, - RouterModule, - JslibModule, - WebLayoutModule, - IconModule, - LinkModule, - BannerModule, - ], + imports: [CommonModule, RouterModule, JslibModule, WebLayoutModule, IconModule], }) export class ProvidersLayoutComponent implements OnInit, OnDestroy { protected readonly logo = ProviderPortalLogo; @@ -41,15 +29,9 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy { protected isBillable: Observable; protected canAccessBilling$: Observable; - protected showProviderClientVaultPrivacyWarningBanner$ = this.configService.getFeatureFlag$( - FeatureFlag.ProviderClientVaultPrivacyBanner, - ); - constructor( private route: ActivatedRoute, private providerService: ProviderService, - private configService: ConfigService, - protected providerClientVaultPrivacyBannerService: ProviderClientVaultPrivacyBannerService, ) {} ngOnInit() { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 092762633321..00c944e69bb1 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -2,10 +2,8 @@ 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 { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; @@ -14,12 +12,10 @@ import { ProviderSubscriptionComponent, hasConsolidatedBilling, ProviderBillingHistoryComponent, - vNextManageClientsComponent, } from "../../billing/providers"; import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; -import { vNextClientsComponent } from "./clients/vnext-clients.component"; import { providerPermissionsGuard } from "./guards/provider-permissions.guard"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { EventsComponent } from "./manage/events.component"; @@ -86,25 +82,13 @@ const routes: Routes = [ children: [ { path: "", pathMatch: "full", redirectTo: "clients" }, { path: "clients/create", component: CreateOrganizationComponent }, - ...featureFlaggedRoute({ - defaultComponent: ClientsComponent, - flaggedComponent: vNextClientsComponent, - featureFlag: FeatureFlag.PM12443RemovePagingLogic, - routeOptions: { - path: "clients", - data: { titleId: "clients" }, - }, - }), - ...featureFlaggedRoute({ - defaultComponent: ManageClientsComponent, - flaggedComponent: vNextManageClientsComponent, - featureFlag: FeatureFlag.PM12443RemovePagingLogic, - routeOptions: { - path: "manage-client-organizations", - data: { titleId: "clients" }, - canActivate: [hasConsolidatedBilling], - }, - }), + { path: "clients", component: ClientsComponent, data: { titleId: "clients" } }, + { + path: "manage-client-organizations", + canActivate: [hasConsolidatedBilling], + component: ManageClientsComponent, + data: { titleId: "clients" }, + }, { path: "manage", children: [ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 80108e66edad..3310be7ba362 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -11,17 +11,15 @@ import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { CreateClientDialogComponent, - NoClientsComponent, ManageClientNameDialogComponent, - ManageClientsComponent, ManageClientSubscriptionDialogComponent, ProviderBillingHistoryComponent, ProviderSubscriptionComponent, ProviderSubscriptionStatusComponent, } from "../../billing/providers"; +import { AddExistingOrganizationDialogComponent } from "../../billing/providers/clients/add-existing-organization-dialog.component"; import { AddOrganizationComponent } from "./clients/add-organization.component"; -import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; @@ -59,7 +57,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr AddOrganizationComponent, BulkConfirmDialogComponent, BulkRemoveDialogComponent, - ClientsComponent, CreateOrganizationComponent, EventsComponent, MembersComponent, @@ -67,9 +64,8 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr SetupProviderComponent, UserAddEditComponent, AddEditMemberDialogComponent, + AddExistingOrganizationDialogComponent, CreateClientDialogComponent, - NoClientsComponent, - ManageClientsComponent, ManageClientNameDialogComponent, ManageClientSubscriptionDialogComponent, ProviderBillingHistoryComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/provider-client-vault-privacy-banner.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/provider-client-vault-privacy-banner.service.ts deleted file mode 100644 index c347f5c2aaeb..000000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/provider-client-vault-privacy-banner.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { - StateProvider, - AC_BANNERS_DISMISSED_DISK, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; - -export const SHOW_BANNER_KEY = new UserKeyDefinition( - AC_BANNERS_DISMISSED_DISK, - "showProviderClientVaultPrivacyBanner", - { - deserializer: (b) => b, - clearOn: [], - }, -); - -/** Displays a banner warning provider users that client organization vaults - * will soon become inaccessible directly. */ -@Injectable({ providedIn: "root" }) -export class ProviderClientVaultPrivacyBannerService { - private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); - - showBanner$ = this._showBanner.state$; - - constructor(private stateProvider: StateProvider) {} - - async hideBanner() { - await this._showBanner.update(() => false); - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index 264b43aee9dc..d3482ea67a5c 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -1,15 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; +import { switchMap } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; import { CreateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/create-client-organization.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; @@ -23,6 +28,8 @@ export class WebProviderService { private i18nService: I18nService, private encryptService: EncryptService, private billingApiService: BillingApiServiceAbstraction, + private stateProvider: StateProvider, + private providerApiService: ProviderApiServiceAbstraction, ) {} async addOrganizationToProvider(providerId: string, organizationId: string) { @@ -40,6 +47,22 @@ export class WebProviderService { return response; } + async addOrganizationToProviderVNext(providerId: string, organizationId: string): Promise { + const orgKey = await firstValueFrom( + this.stateProvider.activeUserId$.pipe( + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), + ), + ); + const providerKey = await this.keyService.getProviderKey(providerId); + const encryptedOrgKey = await this.encryptService.encrypt(orgKey.key, providerKey); + await this.providerApiService.addOrganizationToProvider(providerId, { + key: encryptedOrgKey.encryptedString, + organizationId, + }); + await this.syncService.fullSync(true); + } + async createClientOrganization( providerId: string, name: string, diff --git a/bitwarden_license/bit-web/src/app/app.component.ts b/bitwarden_license/bit-web/src/app/app.component.ts index 1e0f60e2cd20..dd814f5c0d24 100644 --- a/bitwarden_license/bit-web/src/app/app.component.ts +++ b/bitwarden_license/bit-web/src/app/app.component.ts @@ -20,17 +20,10 @@ export class AppComponent extends BaseAppComponent implements OnInit { this.policyListService.addPolicies([ new MaximumVaultTimeoutPolicy(), new DisablePersonalVaultExportPolicy(), + new FreeFamiliesSponsorshipPolicy(), + new ActivateAutofillPolicy(), ]); - this.configService - .getFeatureFlag(FeatureFlag.DisableFreeFamiliesSponsorship) - .then((isFreeFamilyEnabled) => { - if (isFreeFamilyEnabled) { - this.policyListService.addPolicies([new FreeFamiliesSponsorshipPolicy()]); - } - this.policyListService.addPolicies([new ActivateAutofillPolicy()]); - }); - this.configService.getFeatureFlag(FeatureFlag.IdpAutoSubmitLogin).then((enabled) => { if ( enabled && diff --git a/bitwarden_license/bit-web/src/app/app.module.ts b/bitwarden_license/bit-web/src/app/app.module.ts index 3a78ae0ed015..833a67e15156 100644 --- a/bitwarden_license/bit-web/src/app/app.module.ts +++ b/bitwarden_license/bit-web/src/app/app.module.ts @@ -4,7 +4,6 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouterModule } from "@angular/router"; -import { InfiniteScrollDirective } from "ngx-infinite-scroll"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CoreModule } from "@bitwarden/web-vault/app/core"; @@ -37,7 +36,6 @@ import { AccessIntelligenceModule } from "./tools/access-intelligence/access-int FormsModule, ReactiveFormsModule, CoreModule, - InfiniteScrollDirective, DragDropModule, AppRoutingModule, OssRoutingModule, diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 6449ef7a7013..779cb96146c4 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -9,13 +9,17 @@ import { Validators, } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Observable, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; 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 { + getOrganizationById, + 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 { MemberDecryptionType, OpenIdConnectRedirectBehavior, @@ -28,6 +32,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -195,6 +200,7 @@ export class SsoComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private configService: ConfigService, private toastService: ToastService, @@ -260,7 +266,12 @@ export class SsoComponent implements OnInit, OnDestroy { } async load() { - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const ssoSettings = await this.organizationApiService.getSso(this.organizationId); this.populateForm(ssoSettings); diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.html new file mode 100644 index 000000000000..a22484ed92d6 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.html @@ -0,0 +1,73 @@ + + + {{ "addExistingOrganization" | i18n }} + + + +

{{ "selectOrganizationProviderPortal" | i18n }}

+ + + + {{ "name" | i18n }} + {{ "assigned" | i18n }} + + + + + + + + + {{ addable.name }} +
+ {{ "assignedExceedsAvailable" | i18n }} +
+ + {{ addable.seats }} + + + + +
+
+

+ {{ "noOrganizations" | i18n }} +

+
+ +

{{ "yourProviderSubscriptionCredit" | i18n }}

+

{{ "doYouWantToAddThisOrg" | i18n: dialogParams.provider.name }}

+
+
{{ "organization" | i18n }}: {{ selectedOrganization.name }}
+
{{ "billingPlan" | i18n }}: {{ selectedOrganization.plan }}
+
{{ "assignedSeats" | i18n }}: {{ selectedOrganization.seats }}
+
+
+
+ + + + +
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts new file mode 100644 index 000000000000..3df0693d091e --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts @@ -0,0 +1,82 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; + +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; + +export type AddExistingOrganizationDialogParams = { + provider: Provider; +}; + +export enum AddExistingOrganizationDialogResultType { + Closed = "closed", + Submitted = "submitted", +} + +@Component({ + templateUrl: "./add-existing-organization-dialog.component.html", +}) +export class AddExistingOrganizationDialogComponent implements OnInit { + protected loading: boolean = true; + + addableOrganizations: AddableOrganizationResponse[] = []; + selectedOrganization?: AddableOrganizationResponse; + + protected readonly ResultType = AddExistingOrganizationDialogResultType; + + constructor( + @Inject(DIALOG_DATA) protected dialogParams: AddExistingOrganizationDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + private providerApiService: ProviderApiServiceAbstraction, + private toastService: ToastService, + private webProviderService: WebProviderService, + ) {} + + async ngOnInit() { + this.addableOrganizations = await this.providerApiService.getProviderAddableOrganizations( + this.dialogParams.provider.id, + ); + this.loading = false; + } + + addExistingOrganization = async (): Promise => { + if (this.selectedOrganization) { + await this.webProviderService.addOrganizationToProviderVNext( + this.dialogParams.provider.id, + this.selectedOrganization.id, + ); + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("addedExistingOrganization"), + }); + + this.dialogRef.close(this.ResultType.Submitted); + } + }; + + selectOrganization(organizationId: string) { + this.selectedOrganization = this.addableOrganizations.find( + (organization) => organization.id === organizationId, + ); + } + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig< + AddExistingOrganizationDialogParams, + DialogRef + >, + ) => + dialogService.open< + AddExistingOrganizationDialogResultType, + AddExistingOrganizationDialogParams + >(AddExistingOrganizationDialogComponent, dialogConfig); +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html index 78f2cb41bef5..c11b23db9fb2 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html @@ -15,7 +15,7 @@
@@ -29,9 +29,11 @@

{{ planCard.name }}

{{ - planCard.cost | currency: "$" + planCard.getMonthlyCost() | currency: "$" }} - / {{ "monthPerMember" | i18n }} + / {{ planCard.getTimePerMemberLabel() | i18n }}
@@ -45,8 +47,8 @@

{{ planCard.name }}

{{ "organizationNameMaxLength" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index 2a27b1b32f31..38eaed839378 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -1,6 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { BasePortalOutlet } from "@angular/cdk/portal"; import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; @@ -25,48 +24,42 @@ export enum CreateClientDialogResultType { export const openCreateClientDialog = ( dialogService: DialogService, - dialogConfig: DialogConfig, + dialogConfig: DialogConfig< + CreateClientDialogParams, + DialogRef, + BasePortalOutlet + >, ) => dialogService.open( CreateClientDialogComponent, dialogConfig, ); -type PlanCard = { - name: string; - cost: number; - type: PlanType; - plan: PlanResponse; +export class PlanCard { + readonly name: string; + private readonly cost: number; + readonly type: PlanType; + readonly plan: PlanResponse; selected: boolean; -}; -@Component({ - templateUrl: "./create-client-dialog.component.html", -}) -export class CreateClientDialogComponent implements OnInit { - protected discountPercentage: number; - protected formGroup = new FormGroup({ - clientOwnerEmail: new FormControl("", [Validators.required, Validators.email]), - organizationName: new FormControl("", [Validators.required, Validators.maxLength(50)]), - seats: new FormControl(null, [Validators.required, Validators.min(1)]), - }); - protected loading = true; - protected planCards: PlanCard[]; - protected ResultType = CreateClientDialogResultType; + constructor(name: string, cost: number, type: PlanType, plan: PlanResponse, selected: boolean) { + this.name = name; + this.cost = cost; + this.type = type; + this.plan = plan; + this.selected = selected; + } - private providerPlans: ProviderPlanResponse[]; + getMonthlyCost(): number { + return this.plan.isAnnual ? this.cost / 12 : this.cost; + } - constructor( - private billingApiService: BillingApiServiceAbstraction, - @Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams, - private dialogRef: DialogRef, - private i18nService: I18nService, - private toastService: ToastService, - private webProviderService: WebProviderService, - ) {} + getTimePerMemberLabel(): string { + return this.plan.isAnnual ? "monthPerMemberBilledAnnually" : "monthPerMember"; + } - protected getPlanCardContainerClasses(selected: boolean) { - switch (selected) { + getContainerClasses() { + switch (this.selected) { case true: { return [ "tw-group/plan-card-container", @@ -97,6 +90,41 @@ export class CreateClientDialogComponent implements OnInit { } } } +} + +@Component({ + templateUrl: "./create-client-dialog.component.html", +}) +export class CreateClientDialogComponent implements OnInit { + protected discountPercentage: number | null | undefined; + protected formGroup = new FormGroup({ + clientOwnerEmail: new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.email], + }), + organizationName: new FormControl("", { + nonNullable: true, + validators: [Validators.required, Validators.maxLength(50)], + }), + seats: new FormControl(1, { + nonNullable: true, + validators: [Validators.required, Validators.min(1)], + }), + }); + protected loading = true; + protected planCards: PlanCard[] = []; + protected ResultType = CreateClientDialogResultType; + + private providerPlans: ProviderPlanResponse[] = []; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + @Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + private toastService: ToastService, + private webProviderService: WebProviderService, + ) {} async ngOnInit(): Promise { const response = await this.billingApiService.getProviderSubscription( @@ -114,6 +142,10 @@ export class CreateClientDialogComponent implements OnInit { const providerPlan = this.providerPlans[i]; const plan = this.dialogParams.plans.find((plan) => plan.type === providerPlan.type); + if (!plan) { + continue; + } + let planName: string; switch (plan.productTier) { case ProductTierType.Teams: { @@ -124,23 +156,28 @@ export class CreateClientDialogComponent implements OnInit { planName = this.i18nService.t("planNameEnterprise"); break; } + default: + continue; } - this.planCards.push({ - name: planName, - cost: plan.PasswordManager.providerPortalSeatPrice * discountFactor, - type: plan.type, - plan: plan, - selected: i === 0, - }); + this.planCards.push( + new PlanCard( + planName, + plan.PasswordManager.providerPortalSeatPrice * discountFactor, + plan.type, + plan, + i === 0, + ), + ); } this.loading = false; } protected selectPlan(name: string) { - this.planCards.find((planCard) => planCard.name === name).selected = true; - this.planCards.find((planCard) => planCard.name !== name).selected = false; + this.planCards.forEach((planCard) => { + planCard.selected = planCard.name === name; + }); } submit = async () => { @@ -152,17 +189,21 @@ export class CreateClientDialogComponent implements OnInit { const selectedPlanCard = this.planCards.find((planCard) => planCard.selected); + if (!selectedPlanCard) { + return; + } + await this.webProviderService.createClientOrganization( this.dialogParams.providerId, - this.formGroup.value.organizationName, - this.formGroup.value.clientOwnerEmail, + this.formGroup.controls.organizationName.value, + this.formGroup.controls.clientOwnerEmail.value, selectedPlanCard.type, - this.formGroup.value.seats, + this.formGroup.controls.seats.value, ); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("createdNewClient"), }); @@ -178,7 +219,7 @@ export class CreateClientDialogComponent implements OnInit { const openSeats = selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats; - const unassignedSeats = openSeats - this.formGroup.value.seats; + const unassignedSeats = openSeats - this.formGroup.controls.seats.value; return unassignedSeats > 0 ? unassignedSeats : 0; } @@ -191,22 +232,22 @@ export class CreateClientDialogComponent implements OnInit { } if (selectedProviderPlan.purchasedSeats > 0) { - return this.formGroup.value.seats; + return this.formGroup.controls.seats.value; } const additionalSeatsPurchased = - this.formGroup.value.seats + + this.formGroup.controls.seats.value + selectedProviderPlan.assignedSeats - selectedProviderPlan.seatMinimum; return additionalSeatsPurchased > 0 ? additionalSeatsPurchased : 0; } - private getSelectedProviderPlan(): ProviderPlanResponse { + private getSelectedProviderPlan(): ProviderPlanResponse | null { if (this.loading || !this.planCards) { return null; } - const selectedPlan = this.planCards.find((planCard) => planCard.selected).plan; - return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name); + const selectedPlan = this.planCards.find((planCard) => planCard.selected)!.plan; + return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name)!; } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts index 05887fc198e0..898d51e0bafd 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts @@ -3,5 +3,4 @@ export * from "./manage-clients.component"; export * from "./manage-client-name-dialog.component"; export * from "./manage-client-subscription-dialog.component"; export * from "./no-clients.component"; -export * from "./vnext-manage-clients.component"; export * from "./replace.pipe"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html index 49a9208107f7..077aeb6c1248 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html @@ -1,9 +1,39 @@ - - - - {{ "addNewOrganization" | i18n }} - + + + + + + + + + + + + {{ "addNewOrganization" | i18n }} + + @@ -16,78 +46,67 @@ - + - - {{ "client" | i18n }} - {{ "assigned" | i18n }} - {{ "used" | i18n }} - {{ "remaining" | i18n }} - {{ "billingPlan" | i18n }} - - + {{ "client" | i18n }} + {{ "assigned" | i18n }} + {{ "used" | i18n }} + {{ "remaining" | i18n }} + {{ "billingPlan" | i18n }} + - - - - - - - - - - {{ client.seats }} - - - {{ client.occupiedSeats }} - - - {{ client.remainingSeats }} - - - {{ removeMonthly(client.plan) }} - - - - - - - - - - + + + + + + + + + {{ row.seats }} + + + {{ row.occupiedSeats }} + + + {{ row.remainingSeats }} + + + {{ row.plan | replace: " (Monthly)" : "" }} + + + + + + + + + - -
+ +
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts index d413551b7433..074343691224 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts @@ -1,25 +1,36 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, from, lastValueFrom, map } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { debounceTime, first, switchMap } from "rxjs/operators"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; -import { BaseClientsComponent } from "../../../admin-console/providers/clients/base-clients.component"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; +import { + AddExistingOrganizationDialogComponent, + AddExistingOrganizationDialogResultType, +} from "./add-existing-organization-dialog.component"; import { CreateClientDialogResultType, openCreateClientDialog, @@ -32,47 +43,57 @@ import { ManageClientSubscriptionDialogResultType, openManageClientSubscriptionDialog, } from "./manage-client-subscription-dialog.component"; +import { NoClientsComponent } from "./no-clients.component"; +import { ReplacePipe } from "./replace.pipe"; @Component({ templateUrl: "manage-clients.component.html", + standalone: true, + imports: [ + AvatarModule, + TableModule, + HeaderModule, + SharedOrganizationModule, + NoClientsComponent, + ReplacePipe, + ], }) -export class ManageClientsComponent extends BaseClientsComponent { - providerId: string; - provider: Provider; - +export class ManageClientsComponent { + providerId: string = ""; + provider: Provider | undefined; loading = true; isProviderAdmin = false; + dataSource: TableDataSource = + new TableDataSource(); - protected plans: PlanResponse[]; + protected searchControl = new FormControl("", { nonNullable: true }); + protected plans: PlanResponse[] = []; + protected addExistingOrgsFromProviderPortal$ = this.configService.getFeatureFlag$( + FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal, + ); constructor( - private billingApiService: BillingApiService, + private billingApiService: BillingApiServiceAbstraction, private providerService: ProviderService, private router: Router, - activatedRoute: ActivatedRoute, - dialogService: DialogService, - i18nService: I18nService, - searchService: SearchService, - toastService: ToastService, - validationService: ValidationService, - webProviderService: WebProviderService, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + private configService: ConfigService, ) { - super( - activatedRoute, - dialogService, - i18nService, - searchService, - toastService, - validationService, - webProviderService, - ); - - this.activatedRoute.parent.params - .pipe( + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); + + this.activatedRoute.parent?.params + ?.pipe( switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( - map((provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((provider: Provider) => provider?.providerStatus === ProviderStatusType.Billable), map((isBillable) => { if (!isBillable) { return from( @@ -89,26 +110,41 @@ export class ManageClientsComponent extends BaseClientsComponent { takeUntilDestroyed(), ) .subscribe(); - } - removeMonthly = (plan: string) => plan.replace(" (Monthly)", ""); + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); + } async load() { this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); - - this.isProviderAdmin = this.provider.type === ProviderUserType.ProviderAdmin; - - this.clients = ( + this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; + this.dataSource.data = ( await this.billingApiService.getProviderClientOrganizations(this.providerId) ).data; - - this.dataSource.data = this.clients; - this.plans = (await this.billingApiService.getPlans()).data; - this.loading = false; } + addExistingOrganization = async () => { + if (this.provider) { + const reference = AddExistingOrganizationDialogComponent.open(this.dialogService, { + data: { + provider: this.provider, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === AddExistingOrganizationDialogResultType.Submitted) { + await this.load(); + } + } + }; + createClient = async () => { const reference = openCreateClientDialog(this.dialogService, { data: { @@ -131,7 +167,7 @@ export class ManageClientsComponent extends BaseClientsComponent { organization: { id: organization.id, name: organization.organizationName, - seats: organization.seats, + seats: organization.seats ? organization.seats : 0, }, }, }); @@ -149,7 +185,7 @@ export class ManageClientsComponent extends BaseClientsComponent { const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { data: { organization, - provider: this.provider, + provider: this.provider!, }, }); @@ -159,4 +195,28 @@ export class ManageClientsComponent extends BaseClientsComponent { await this.load(); } }; + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts index 30ab444fe8be..7e4323d76034 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { svgIcon } from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; const gearIcon = svgIcon` @@ -23,6 +24,8 @@ const gearIcon = svgIcon` @Component({ selector: "app-no-clients", + standalone: true, + imports: [SharedOrganizationModule], template: `

{{ "noClients" | i18n }}

diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html deleted file mode 100644 index 7c560e49579a..000000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - {{ "addNewOrganization" | i18n }} - - - - - - {{ "loading" | i18n }} - - - - - - {{ "client" | i18n }} - {{ "assigned" | i18n }} - {{ "used" | i18n }} - {{ "remaining" | i18n }} - {{ "billingPlan" | i18n }} - - - - - - - - - - - {{ row.seats }} - - - {{ row.occupiedSeats }} - - - {{ row.remainingSeats }} - - - {{ row.plan | replace: " (Monthly)" : "" }} - - - - - - - - - - - -
- -
-
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts deleted file mode 100644 index 94f615f0cee5..000000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, from, lastValueFrom, map } from "rxjs"; -import { debounceTime, first, switchMap } from "rxjs/operators"; - -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { - AvatarModule, - DialogService, - TableDataSource, - TableModule, - ToastService, -} from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; - -import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; - -import { - CreateClientDialogResultType, - openCreateClientDialog, -} from "./create-client-dialog.component"; -import { - ManageClientNameDialogResultType, - openManageClientNameDialog, -} from "./manage-client-name-dialog.component"; -import { - ManageClientSubscriptionDialogResultType, - openManageClientSubscriptionDialog, -} from "./manage-client-subscription-dialog.component"; -import { ReplacePipe } from "./replace.pipe"; -import { vNextNoClientsComponent } from "./vnext-no-clients.component"; - -@Component({ - templateUrl: "vnext-manage-clients.component.html", - standalone: true, - imports: [ - AvatarModule, - TableModule, - HeaderModule, - SharedOrganizationModule, - vNextNoClientsComponent, - ReplacePipe, - ], -}) -export class vNextManageClientsComponent { - providerId: string = ""; - provider: Provider | undefined; - loading = true; - isProviderAdmin = false; - dataSource: TableDataSource = - new TableDataSource(); - - protected searchControl = new FormControl("", { nonNullable: true }); - protected plans: PlanResponse[] = []; - - constructor( - private billingApiService: BillingApiServiceAbstraction, - private providerService: ProviderService, - private router: Router, - private activatedRoute: ActivatedRoute, - private dialogService: DialogService, - private i18nService: I18nService, - private toastService: ToastService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - ) { - this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { - this.searchControl.setValue(queryParams.search); - }); - - this.activatedRoute.parent?.params - ?.pipe( - switchMap((params) => { - this.providerId = params.providerId; - return this.providerService.get$(this.providerId).pipe( - map((provider: Provider) => provider?.providerStatus === ProviderStatusType.Billable), - map((isBillable) => { - if (!isBillable) { - return from( - this.router.navigate(["../clients"], { - relativeTo: this.activatedRoute, - }), - ); - } else { - return from(this.load()); - } - }), - ); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((searchText) => { - this.dataSource.filter = (data) => - data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; - }); - } - - async load() { - this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); - - this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; - - const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) - .data; - - this.dataSource.data = clients; - - this.plans = (await this.billingApiService.getPlans()).data; - - this.loading = false; - } - - createClient = async () => { - const reference = openCreateClientDialog(this.dialogService, { - data: { - providerId: this.providerId, - plans: this.plans, - }, - }); - - const result = await lastValueFrom(reference.closed); - - if (result === CreateClientDialogResultType.Submitted) { - await this.load(); - } - }; - - manageClientName = async (organization: ProviderOrganizationOrganizationDetailsResponse) => { - const dialogRef = openManageClientNameDialog(this.dialogService, { - data: { - providerId: this.providerId, - organization: { - id: organization.id, - name: organization.organizationName, - seats: organization.seats ? organization.seats : 0, - }, - }, - }); - - const result = await firstValueFrom(dialogRef.closed); - - if (result === ManageClientNameDialogResultType.Submitted) { - await this.load(); - } - }; - - manageClientSubscription = async ( - organization: ProviderOrganizationOrganizationDetailsResponse, - ) => { - const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { - data: { - organization, - provider: this.provider!, - }, - }); - - const result = await firstValueFrom(dialogRef.closed); - - if (result === ManageClientSubscriptionDialogResultType.Submitted) { - await this.load(); - } - }; - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - await this.webProviderService.detachOrganization(this.providerId, organization.id); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("detachedOrganization", organization.organizationName), - }); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts deleted file mode 100644 index 5ad19945c51e..000000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; - -import { svgIcon } from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; - -const gearIcon = svgIcon` - - - - - - - - - - - - - - - - -`; - -@Component({ - selector: "app-no-clients", - standalone: true, - imports: [SharedOrganizationModule], - template: `
- -

{{ "noClients" | i18n }}

- - - {{ "addNewOrganization" | i18n }} - -
`, -}) -export class vNextNoClientsComponent { - icon = gearIcon; - @Input() showAddOrganizationButton = true; - @Output() addNewOrganizationClicked = new EventEmitter(); - - addNewOrganization = () => this.addNewOrganizationClicked.emit(); -} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 50db2cb6a364..72f743d2cc7c 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -25,7 +25,7 @@ {{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{ - i.cadence.toLowerCase() + getFormattedPlanNameCadence(i.cadence) | i18n }}) {{ "×" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }} @ {{ @@ -38,12 +38,11 @@ }} - {{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} /{{ - "month" | i18n - }} + {{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} / + {{ getBillingCadenceLabel(i) | i18n }}
- {{ i.cost | currency: "$" }} /{{ "month" | i18n }} + {{ i.cost | currency: "$" }} / {{ getBillingCadenceLabel(i) | i18n }}
@@ -52,8 +51,9 @@ - Total: {{ totalCost | currency: "$" }} /{{ - "month" | i18n + Total: {{ totalCost | currency: "$" }} / + {{ + getBillingCadenceLabel(activePlans.length > 0 ? activePlans[0] : null) | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index a6b27b9f3dd9..02fa29578d9a 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -19,14 +19,13 @@ import { ToastService } from "@bitwarden/components"; templateUrl: "./provider-subscription.component.html", }) export class ProviderSubscriptionComponent implements OnInit, OnDestroy { - providerId: string; - subscription: ProviderSubscriptionResponse; + private providerId: string; + protected subscription: ProviderSubscriptionResponse; - firstLoaded = false; - loading: boolean; + protected firstLoaded = false; + protected loading: boolean; private destroy$ = new Subject(); - totalCost: number; - currentDate = new Date(); + protected totalCost: number; protected readonly TaxInformation = TaxInformation; @@ -50,11 +49,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { .subscribe(); } - get isExpired() { - return this.subscription.status !== "active"; - } - - async load() { + protected async load() { if (this.loading) { return; } @@ -65,7 +60,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { this.loading = false; } - updateTaxInformation = async (taxInformation: TaxInformation) => { + protected updateTaxInformation = async (taxInformation: TaxInformation) => { const request = ExpandedTaxInfoUpdateRequest.From(taxInformation); await this.billingApiService.updateProviderTaxInformation(this.providerId, request); this.toastService.showToast({ @@ -75,7 +70,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { }); }; - getFormattedCost( + protected getFormattedCost( cost: number, seatMinimum: number, purchasedSeats: number, @@ -85,17 +80,21 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { return costPerSeat - (costPerSeat * discountPercentage) / 100; } - getFormattedPlanName(planName: string): string { + protected getFormattedPlanName(planName: string): string { const spaceIndex = planName.indexOf(" "); return planName.substring(0, spaceIndex); } - getFormattedSeatCount(seatMinimum: number, purchasedSeats: number): string { + protected getFormattedSeatCount(seatMinimum: number, purchasedSeats: number): string { const totalSeats = seatMinimum + purchasedSeats; return totalSeats > 1 ? totalSeats.toString() : ""; } - sumCost(plans: ProviderPlanResponse[]): number { + protected getFormattedPlanNameCadence(cadence: string) { + return cadence === "Annual" ? "annually" : "monthly"; + } + + private sumCost(plans: ProviderPlanResponse[]): number { return plans.reduce((acc, plan) => acc + plan.cost, 0); } @@ -104,7 +103,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - get activePlans(): ProviderPlanResponse[] { + protected get activePlans(): ProviderPlanResponse[] { return this.subscription.plans.filter((plan) => { if (plan.purchasedSeats === 0) { return plan.seatMinimum > 0; @@ -113,4 +112,19 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { } }); } + + protected getBillingCadenceLabel(providerPlanResponse: ProviderPlanResponse): string { + if (providerPlanResponse == null || providerPlanResponse == undefined) { + return "month"; + } + + switch (providerPlanResponse.cadence) { + case "Monthly": + return "month"; + case "Annual": + return "year"; + default: + return "month"; + } + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts index a1f7564156c6..d042c4d904e8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts @@ -1,7 +1,13 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; +import { firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; /** @@ -10,13 +16,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv export const organizationEnabledGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { const syncService = inject(SyncService); const orgService = inject(OrganizationService); + const accountService = inject(AccountService); /** Workaround to avoid service initialization race condition. */ if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const org = await orgService.get(route.params.organizationId); + const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); + const org = await firstValueFrom( + orgService.organizations$(userId).pipe(getOrganizationById(route.params.organizationId)), + ); if (org == null || !org.canAccessSecretsManager) { return createUrlTreeFromSnapshot(route, ["/"]); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts index 2a36bf1cbbcb..39576bc8dc65 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts @@ -5,8 +5,11 @@ import { createUrlTreeFromSnapshot, RouterStateSnapshot, } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; /** @@ -18,13 +21,15 @@ export const canActivateSM: CanActivateFn = async ( ) => { const syncService = inject(SyncService); const orgService = inject(OrganizationService); + const accountService = inject(AccountService); /** Workaround to avoid service initialization race condition. */ if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const orgs = await orgService.getAll(); + const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); + const orgs = await firstValueFrom(orgService.organizations$(userId)); const smOrg = orgs.find((o) => o.canAccessSecretsManager); if (smOrg) { return createUrlTreeFromSnapshot(route, ["/sm", smOrg.id]); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index adf01afd10cc..6594b71a14ce 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -15,8 +15,13 @@ import { takeUntil, } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SecretsManagerLogo } from "@bitwarden/web-vault/app/layouts/secrets-manager-logo"; import { OrganizationCounts } from "../models/view/counts.view"; @@ -41,6 +46,7 @@ export class NavigationComponent implements OnInit, OnDestroy { constructor( protected route: ActivatedRoute, private organizationService: OrganizationService, + private accountService: AccountService, private countService: CountService, private projectService: ProjectService, private secretService: SecretService, @@ -50,7 +56,15 @@ export class NavigationComponent implements OnInit, OnDestroy { ngOnInit() { const org$ = this.route.params.pipe( - concatMap((params) => this.organizationService.get(params.organizationId)), + concatMap((params) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), distinctUntilChanged(), takeUntil(this.destroy$), ); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index a95192e0d912..7eb28b2bc2d9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -20,15 +20,20 @@ import { import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; 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 { + getOrganizationById, + 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; 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 { TrialFlowService } from "@bitwarden/web-vault/app/billing/services/trial-flow.service"; -import { FreeTrial } from "@bitwarden/web-vault/app/core/types/free-trial"; +import { FreeTrial } from "@bitwarden/web-vault/app/billing/types/free-trial"; import { OrganizationCounts } from "../models/view/counts.view"; import { ProjectListView } from "../models/view/project-list.view"; @@ -112,6 +117,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private serviceAccountService: ServiceAccountService, private dialogService: DialogService, private organizationService: OrganizationService, + private accountService: AccountService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private smOnboardingTasksService: SMOnboardingTasksService, @@ -130,7 +136,15 @@ export class OverviewComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - const org$ = orgId$.pipe(switchMap((orgId) => this.organizationService.get(orgId))); + const org$ = orgId$.pipe( + switchMap((orgId) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe(getOrganizationById(orgId)), + ), + ), + ), + ); org$.pipe(takeUntil(this.destroy$)).subscribe((org) => { this.organizationId = org.id; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts index 78d367776d40..159b90d54321 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts @@ -3,10 +3,15 @@ import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; +import { of } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { RouterService } from "@bitwarden/web-vault/app/core"; @@ -32,6 +37,8 @@ describe("Project Redirect Guard", () => { let i18nServiceMock: MockProxy; let toastService: MockProxy; let router: Router; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; const projectView = { @@ -50,6 +57,7 @@ describe("Project Redirect Guard", () => { projectServiceMock = mock(); i18nServiceMock = mock(); toastService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ imports: [ @@ -71,6 +79,7 @@ describe("Project Redirect Guard", () => { ], providers: [ { provide: OrganizationService, useValue: organizationService }, + { provide: AccountService, useValue: accountService }, { provide: RouterService, useValue: routerService }, { provide: ProjectService, useValue: projectServiceMock }, { provide: I18nService, useValue: i18nServiceMock }, @@ -83,7 +92,7 @@ describe("Project Redirect Guard", () => { it("redirects to sm/{orgId}/projects/{projectId} if project exists", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); projectServiceMock.getByProjectId.mockReturnValue(Promise.resolve(projectView)); // Act @@ -95,7 +104,7 @@ describe("Project Redirect Guard", () => { it("redirects to sm/projects if project does not exist", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); // Act await router.navigateByUrl("sm/123/projects/124"); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts index ee2395b3f830..8c9f894f8f6e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts @@ -4,8 +4,8 @@ import { Injectable } from "@angular/core"; import { Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { KeyService } from "@bitwarden/key-management"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html index 1ab8b7e0196d..d9919ef6bac2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html @@ -2,7 +2,7 @@
- - diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts deleted file mode 100644 index d6a757fe8974..000000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { MemberDetailsFlat } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; -import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; - -type AppAtRiskMembersDialogParams = { - members: MemberDetailsFlat[]; - applicationName: string; -}; - -export const openAppAtRiskMembersDialog = ( - dialogService: DialogService, - dialogConfig: AppAtRiskMembersDialogParams, -) => - dialogService.open(AppAtRiskMembersDialogComponent, { - data: dialogConfig, - }); - -@Component({ - standalone: true, - templateUrl: "./app-at-risk-members-dialog.component.html", - imports: [ButtonModule, CommonModule, JslibModule, DialogModule], -}) -export class AppAtRiskMembersDialogComponent { - protected members: MemberDetailsFlat[]; - protected applicationName: string; - - constructor(@Inject(DIALOG_DATA) private params: AppAtRiskMembersDialogParams) { - this.members = params.members; - this.applicationName = params.applicationName; - } -} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts deleted file mode 100644 index 4dffa60b562e..000000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts +++ /dev/null @@ -1,56 +0,0 @@ -export const applicationTableMockData = [ - { - id: 1, - name: "google.com", - atRiskPasswords: 4, - totalPasswords: 10, - atRiskMembers: 2, - totalMembers: 5, - isMarkedAsCritical: false, - }, - { - id: 2, - name: "facebook.com", - atRiskPasswords: 3, - totalPasswords: 8, - atRiskMembers: 1, - totalMembers: 3, - isMarkedAsCritical: false, - }, - { - id: 3, - name: "twitter.com", - atRiskPasswords: 2, - totalPasswords: 6, - atRiskMembers: 0, - totalMembers: 2, - isMarkedAsCritical: false, - }, - { - id: 4, - name: "linkedin.com", - atRiskPasswords: 1, - totalPasswords: 4, - atRiskMembers: 0, - totalMembers: 1, - isMarkedAsCritical: false, - }, - { - id: 5, - name: "instagram.com", - atRiskPasswords: 0, - totalPasswords: 2, - atRiskMembers: 0, - totalMembers: 0, - isMarkedAsCritical: false, - }, - { - id: 6, - name: "tiktok.com", - atRiskPasswords: 0, - totalPasswords: 1, - atRiskMembers: 0, - totalMembers: 0, - isMarkedAsCritical: false, - }, -]; diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html index 1c503f3d7866..4dc4b7ffb1a4 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html @@ -28,24 +28,34 @@

{{ "criticalApplications" | i18n }}

-
@@ -60,38 +70,57 @@

{{ "criticalApplications" | i18n }}

- {{ "application" | i18n }} - {{ "atRiskPasswords" | i18n }} - {{ "totalPasswords" | i18n }} - {{ "atRiskMembers" | i18n }} - {{ "totalMembers" | i18n }} + {{ "application" | i18n }} + {{ "atRiskPasswords" | i18n }} + {{ "totalPasswords" | i18n }} + {{ "atRiskMembers" | i18n }} + {{ "totalMembers" | i18n }} - + - + - - {{ r.name }} + + {{ r.applicationName }} - {{ r.atRiskPasswords }} + {{ r.atRiskPasswordCount }} - {{ r.totalPasswords }} + {{ r.passwordCount }} - {{ r.atRiskMembers }} + {{ r.atRiskMemberCount }} - {{ r.totalMembers }} + {{ r.memberCount }} + + + + + + + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts index 99f68aa9c723..f1fa38dd28f3 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts @@ -4,16 +4,33 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { debounceTime, map } from "rxjs"; +import { combineLatest, debounceTime, map } from "rxjs"; +import { + CriticalAppsService, + RiskInsightsDataService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + ApplicationHealthReportDetailWithCriticalFlag, + ApplicationHealthReportSummary, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +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 { SearchModule, TableDataSource, NoItemsModule, Icons } from "@bitwarden/components"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + Icons, + NoItemsModule, + SearchModule, + TableDataSource, + ToastService, +} from "@bitwarden/components"; import { CardComponent } from "@bitwarden/tools-card"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -import { applicationTableMockData } from "./application-table.mock"; import { RiskInsightsTabType } from "./risk-insights.component"; @Component({ @@ -21,32 +38,45 @@ import { RiskInsightsTabType } from "./risk-insights.component"; selector: "tools-critical-applications", templateUrl: "./critical-applications.component.html", imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule], + providers: [], }) export class CriticalApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); + protected dataSource = new TableDataSource(); protected selectedIds: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); protected loading = false; protected organizationId: string; + protected applicationSummary = {} as ApplicationHealthReportSummary; noItemsIcon = Icons.Security; - // MOCK DATA - protected mockData = applicationTableMockData; - protected mockAtRiskMembersCount = 0; - protected mockAtRiskAppsCount = 0; - protected mockTotalMembersCount = 0; - protected mockTotalAppsCount = 0; + isNotificationsFeatureEnabled: boolean = false; - ngOnInit() { - this.activatedRoute.paramMap + async ngOnInit() { + this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.EnableRiskInsightsNotifications, + ); + this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; + combineLatest([ + this.dataService.applications$, + this.criticalAppsService.getAppsListForOrg(this.organizationId), + ]) .pipe( takeUntilDestroyed(this.destroyRef), - map(async (params) => { - this.organizationId = params.get("organizationId"); - // TODO: use organizationId to fetch data + map(([applications, criticalApps]) => { + const criticalUrls = criticalApps.map((ca) => ca.uri); + const data = applications?.map((app) => ({ + ...app, + isMarkedAsCritical: criticalUrls.includes(app.applicationName), + })) as ApplicationHealthReportDetailWithCriticalFlag[]; + return data?.filter((app) => app.isMarkedAsCritical); }), ) - .subscribe(); + .subscribe((applications) => { + if (applications) { + this.dataSource.data = applications; + this.applicationSummary = this.reportService.generateApplicationsSummary(applications); + } + }); } goToAllAppsTab = async () => { @@ -56,14 +86,65 @@ export class CriticalApplicationsComponent implements OnInit { }); }; + unmarkAsCriticalApp = async (hostname: string) => { + try { + await this.criticalAppsService.dropCriticalApp( + this.organizationId as OrganizationId, + hostname, + ); + } catch { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + return; + } + + this.toastService.showToast({ + message: this.i18nService.t("criticalApplicationSuccessfullyUnmarked"), + variant: "success", + title: this.i18nService.t("success"), + }); + this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname); + }; + constructor( - protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected router: Router, + protected toastService: ToastService, + protected dataService: RiskInsightsDataService, + protected criticalAppsService: CriticalAppsService, + protected reportService: RiskInsightsReportService, + protected i18nService: I18nService, + private configService: ConfigService, ) { - this.dataSource.data = []; //applicationTableMockData; this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) .subscribe((v) => (this.dataSource.filter = v)); } + + showAppAtRiskMembers = async (applicationName: string) => { + const data = { + members: + this.dataSource.data.find((app) => app.applicationName === applicationName) + ?.atRiskMemberDetails ?? [], + applicationName, + }; + this.dataService.setDrawerForAppAtRiskMembers(data, applicationName); + }; + + showOrgAtRiskMembers = async (invokerId: string) => { + const data = this.reportService.generateAtRiskMemberList(this.dataSource.data); + this.dataService.setDrawerForOrgAtRiskMembers(data, invokerId); + }; + + showOrgAtRiskApps = async (invokerId: string) => { + const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data); + this.dataService.setDrawerForOrgAtRiskApps(data, invokerId); + }; + + trackByFunction(_: number, item: ApplicationHealthReportDetailWithCriticalFlag) { + return item.applicationName; + } } diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html deleted file mode 100644 index 298011b21575..000000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html +++ /dev/null @@ -1,25 +0,0 @@ - - - {{ "atRiskApplicationsWithCount" | i18n: atRiskApps.length }} - - -
- {{ "atRiskApplicationsDescription" | i18n }} -
-
{{ "application" | i18n }}
-
{{ "atRiskPasswords" | i18n }}
-
- -
-
{{ app.applicationName }}
-
{{ app.atRiskPasswordCount }}
-
-
-
-
- - - -
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts deleted file mode 100644 index 0ae00f608745..000000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { AtRiskApplicationDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; -import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; - -export const openOrgAtRiskMembersDialog = ( - dialogService: DialogService, - dialogConfig: AtRiskApplicationDetail[], -) => - dialogService.open(OrgAtRiskAppsDialogComponent, { - data: dialogConfig, - }); - -@Component({ - standalone: true, - templateUrl: "./org-at-risk-apps-dialog.component.html", - imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], -}) -export class OrgAtRiskAppsDialogComponent { - constructor(@Inject(DIALOG_DATA) protected atRiskApps: AtRiskApplicationDetail[]) {} -} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html deleted file mode 100644 index 1f1de1036618..000000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html +++ /dev/null @@ -1,25 +0,0 @@ - - - {{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} - - -
- {{ "atRiskMembersDescription" | i18n }} -
-
{{ "email" | i18n }}
-
{{ "atRiskPasswords" | i18n }}
-
- -
-
{{ member.email }}
-
{{ member.atRiskPasswordCount }}
-
-
-
-
- - - -
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts deleted file mode 100644 index 72518843d94f..000000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; -import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; - -export const openOrgAtRiskMembersDialog = ( - dialogService: DialogService, - dialogConfig: AtRiskMemberDetail[], -) => - dialogService.open(OrgAtRiskMembersDialogComponent, { - data: dialogConfig, - }); - -@Component({ - standalone: true, - templateUrl: "./org-at-risk-members-dialog.component.html", - imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], -}) -export class OrgAtRiskMembersDialogComponent { - constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {} -} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts index 1e9e4171bc3c..852e9adafb3a 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts @@ -10,8 +10,12 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TableModule } from "@bitwarden/components"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; @@ -24,6 +28,7 @@ describe("PasswordHealthMembersUriComponent", () => { let fixture: ComponentFixture; let cipherServiceMock: MockProxy; const passwordHealthServiceMock = mock(); + const userId = Utils.newGuid() as UserId; const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); @@ -36,6 +41,7 @@ describe("PasswordHealthMembersUriComponent", () => { { provide: I18nService, useValue: mock() }, { provide: AuditService, useValue: mock() }, { provide: OrganizationService, useValue: mock() }, + { provide: AccountService, useValue: mockAccountServiceWith(userId) }, { provide: PasswordStrengthServiceAbstraction, useValue: mock(), diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index ae8bd94e5f37..12082e888b02 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -1,57 +1,130 @@ -
{{ "accessIntelligence" | i18n }}
-

{{ "riskInsights" | i18n }}

-
- {{ "reviewAtRiskPasswords" | i18n }} -
-
- - {{ - "dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a") - }} - - - {{ "refresh" | i18n }} - - - + +
{{ "accessIntelligence" | i18n }}
+

{{ "riskInsights" | i18n }}

+
+ {{ "reviewAtRiskPasswords" | i18n }} +
+
+ + {{ + "dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a") + }} + + + {{ "refresh" | i18n }} + + + + - -
- - - - - - - - {{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }} - - - - - - - - - - - - - +
+ + + + + + + + {{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }} + + + + + + + + + + + + + + + + + + + + {{ + "atRiskMembersDescription" | i18n + }} +
+
{{ "email" | i18n }}
+
{{ "atRiskPasswords" | i18n }}
+
+ +
+
{{ member.email }}
+
{{ member.atRiskPasswordCount }}
+
+
+
+
+ + + + + +
+ {{ "atRiskMembersWithCount" | i18n: dataService.appAtRiskMembers.members.length }} +
+
+ {{ + "atRiskMembersDescriptionWithApp" | i18n: dataService.appAtRiskMembers.applicationName + }} +
+
+ +
{{ member.email }}
+
+
+
+
+ + + + + + + {{ + "atRiskApplicationsDescription" | i18n + }} +
+
{{ "application" | i18n }}
+
{{ "atRiskPasswords" | i18n }}
+
+ +
+
{{ app.applicationName }}
+
{{ app.atRiskPasswordCount }}
+
+
+
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 5adb0d32945a..20dc320de20e 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -2,22 +2,33 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; -import { Observable, EMPTY } from "rxjs"; +import { EMPTY, Observable } from "rxjs"; import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { - RiskInsightsDataService, CriticalAppsService, - PasswordHealthReportApplicationsResponse, + RiskInsightsDataService, } from "@bitwarden/bit-common/tools/reports/risk-insights"; -import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { + ApplicationHealthReportDetail, + DrawerType, + PasswordHealthReportApplicationsResponse, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; // eslint-disable-next-line no-restricted-imports -- used for dependency injection import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + ButtonModule, + DrawerBodyComponent, + DrawerComponent, + DrawerHeaderComponent, + LayoutComponent, + TabsModule, +} from "@bitwarden/components"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { AllApplicationsComponent } from "./all-applications.component"; @@ -49,6 +60,10 @@ export enum RiskInsightsTabType { PasswordHealthMembersURIComponent, NotifiedMembersTableComponent, TabsModule, + DrawerComponent, + DrawerBodyComponent, + DrawerHeaderComponent, + LayoutComponent, ], }) export class RiskInsightsComponent implements OnInit { @@ -75,7 +90,7 @@ export class RiskInsightsComponent implements OnInit { private route: ActivatedRoute, private router: Router, private configService: ConfigService, - private dataService: RiskInsightsDataService, + protected dataService: RiskInsightsDataService, private criticalAppsService: CriticalAppsService, ) { this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { @@ -135,5 +150,13 @@ export class RiskInsightsComponent implements OnInit { queryParams: { tabIndex: newIndex }, queryParamsHandling: "merge", }); + + // close drawer when tabs are changed + this.dataService.closeDrawer(); + } + + // Get a list of drawer types + get drawerTypes(): typeof DrawerType { + return DrawerType; } } diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts index 321aae165c5b..52ec29010314 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts @@ -93,17 +93,16 @@ export class MemberAccessReportComponent implements OnInit { }); }; - edit = async (user: MemberAccessReportView | null): Promise => { + edit = async (user: MemberAccessReportView): Promise => { const dialog = openUserAddEditDialog(this.dialogService, { data: { + kind: "Edit", name: this.userNamePipe.transform(user), organizationId: this.organizationId, - organizationUserId: user != null ? user.userGuid : null, - allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], - usesKeyConnector: user?.usesKeyConnector, + organizationUserId: user.userGuid, + usesKeyConnector: user.usesKeyConnector, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: MemberDialogTab.Role, - numSeatsUsed: this.dataSource.data.length, }, }); diff --git a/bitwarden_license/bit-web/src/app/vault/services/abstractions/admin-task.abstraction.ts b/bitwarden_license/bit-web/src/app/vault/services/abstractions/admin-task.abstraction.ts new file mode 100644 index 000000000000..014c9daa7838 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/vault/services/abstractions/admin-task.abstraction.ts @@ -0,0 +1,34 @@ +import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; +import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault"; + +/** + * Request type for creating tasks. + * @property cipherId - Optional. The ID of the cipher to create the task for. + * @property type - The type of task to create. Currently defined as "updateAtRiskCredential". + */ +export type CreateTasksRequest = Readonly<{ + cipherId?: CipherId; + type: SecurityTaskType.UpdateAtRiskCredential; +}>; + +export abstract class AdminTaskService { + /** + * Retrieves all tasks for a given organization. + * @param organizationId - The ID of the organization to retrieve tasks for. + * @param status - Optional. The status of the tasks to retrieve. + */ + abstract getAllTasks( + organizationId: OrganizationId, + status?: SecurityTaskStatus | undefined, + ): Promise; + + /** + * Creates multiple tasks for a given organization and sends out notifications to applicable users. + * @param organizationId - The ID of the organization to create tasks for. + * @param tasks - The tasks to create. + */ + abstract bulkCreateTasks( + organizationId: OrganizationId, + tasks: CreateTasksRequest[], + ): Promise; +} diff --git a/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.spec.ts b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.spec.ts new file mode 100644 index 000000000000..49a4c16e159d --- /dev/null +++ b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.spec.ts @@ -0,0 +1,65 @@ +import { MockProxy, mock } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; +import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault"; + +import { CreateTasksRequest } from "./abstractions/admin-task.abstraction"; +import { DefaultAdminTaskService } from "./default-admin-task.service"; + +describe("DefaultAdminTaskService", () => { + let defaultAdminTaskService: DefaultAdminTaskService; + let apiService: MockProxy; + + beforeEach(() => { + apiService = mock(); + defaultAdminTaskService = new DefaultAdminTaskService(apiService); + }); + + describe("getAllTasks", () => { + it("should call the api service with the correct parameters with status", async () => { + const organizationId = "orgId" as OrganizationId; + const status = SecurityTaskStatus.Pending; + const expectedUrl = `/tasks/organization?organizationId=${organizationId}&status=0`; + + await defaultAdminTaskService.getAllTasks(organizationId, status); + + expect(apiService.send).toHaveBeenCalledWith("GET", expectedUrl, null, true, true); + }); + + it("should call the api service with the correct parameters without status", async () => { + const organizationId = "orgId" as OrganizationId; + const expectedUrl = `/tasks/organization?organizationId=${organizationId}`; + + await defaultAdminTaskService.getAllTasks(organizationId); + + expect(apiService.send).toHaveBeenCalledWith("GET", expectedUrl, null, true, true); + }); + }); + + describe("bulkCreateTasks", () => { + it("should call the api service with the correct parameters", async () => { + const organizationId = "orgId" as OrganizationId; + const tasks: CreateTasksRequest[] = [ + { + cipherId: "cipherId-1" as CipherId, + type: SecurityTaskType.UpdateAtRiskCredential, + }, + { + cipherId: "cipherId-2" as CipherId, + type: SecurityTaskType.UpdateAtRiskCredential, + }, + ]; + + await defaultAdminTaskService.bulkCreateTasks(organizationId, tasks); + + expect(apiService.send).toHaveBeenCalledWith( + "POST", + `/tasks/${organizationId}/bulk-create`, + { tasks }, + true, + true, + ); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts new file mode 100644 index 000000000000..520fb744486b --- /dev/null +++ b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + SecurityTask, + SecurityTaskData, + SecurityTaskResponse, + SecurityTaskStatus, +} from "@bitwarden/vault"; + +import { AdminTaskService, CreateTasksRequest } from "./abstractions/admin-task.abstraction"; + +@Injectable() +export class DefaultAdminTaskService implements AdminTaskService { + constructor(private apiService: ApiService) {} + + async getAllTasks( + organizationId: OrganizationId, + status?: SecurityTaskStatus | undefined, + ): Promise { + const queryParams = new URLSearchParams(); + + queryParams.append("organizationId", organizationId); + if (status !== undefined) { + queryParams.append("status", status.toString()); + } + + const r = await this.apiService.send( + "GET", + `/tasks/organization?${queryParams.toString()}`, + null, + true, + true, + ); + const response = new ListResponse(r, SecurityTaskResponse); + + return response.data.map((d) => new SecurityTask(new SecurityTaskData(d))); + } + + async bulkCreateTasks( + organizationId: OrganizationId, + tasks: CreateTasksRequest[], + ): Promise { + await this.apiService.send( + "POST", + `/tasks/${organizationId}/bulk-create`, + { tasks }, + true, + true, + ); + } +} diff --git a/bitwarden_license/bit-web/tsconfig.build.json b/bitwarden_license/bit-web/tsconfig.build.json index 9bebbeb50615..6313ce278635 100644 --- a/bitwarden_license/bit-web/tsconfig.build.json +++ b/bitwarden_license/bit-web/tsconfig.build.json @@ -9,6 +9,6 @@ ], "include": [ "../../apps/web/src/connectors/*.ts", - "../../libs/common/src/platform/services/**/*.worker.ts" + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts" ] } diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index c4304ec2bd9c..82e0b7f57fa0 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -21,11 +21,12 @@ "../../libs/tools/export/vault-export/vault-export-core/src" ], "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], - "@bitwarden/importer/core": ["../../libs/importer/src"], - "@bitwarden/importer/ui": ["../../libs/importer/src/components"], + "@bitwarden/importer-core": ["../../libs/importer/src"], + "@bitwarden/importer-ui": ["../../libs/importer/src/components"], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], + "@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"], "@bitwarden/platform": ["../../libs/platform/src"], + "@bitwarden/ui-common": ["../../libs/ui/common/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/vault": ["../../libs/vault/src"], @@ -45,7 +46,7 @@ "../../apps/web/src/connectors/*.ts", "../../apps/web/src/**/*.stories.ts", "../../apps/web/src/**/*.spec.ts", - "../../libs/common/src/platform/services/**/*.worker.ts", + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts", "src/**/*.stories.ts", "src/**/*.spec.ts" diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000000..2d7c91521f94 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,381 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import angular from "angular-eslint"; +// @ts-ignore +import importPlugin from "eslint-plugin-import"; +import eslintConfigPrettier from "eslint-config-prettier"; +import eslintPluginTailwindCSS from "eslint-plugin-tailwindcss"; +import rxjs from "eslint-plugin-rxjs"; +import angularRxjs from "eslint-plugin-rxjs-angular"; +import storybook from "eslint-plugin-storybook"; + +import platformPlugins from "./libs/eslint/platform/index.mjs"; + +export default tseslint.config( + ...storybook.configs["flat/recommended"], + { + // Everything in this config object targets our TypeScript files (Components, Directives, Pipes etc) + files: ["**/*.ts", "**/*.js"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + //...tseslint.configs.stylistic, + ...angular.configs.tsRecommended, + importPlugin.flatConfigs.recommended, + importPlugin.flatConfigs.typescript, + eslintConfigPrettier, // Disables rules that conflict with Prettier + ], + plugins: { + rxjs: rxjs, + "rxjs-angular": angularRxjs, + "@bitwarden/platform": platformPlugins, + }, + languageOptions: { + parserOptions: { + project: ["./tsconfig.eslint.json"], + sourceType: "module", + ecmaVersion: 2020, + }, + }, + settings: { + "import/parsers": { + "@typescript-eslint/parser": [".ts"], + }, + "import/resolver": { + typescript: { + alwaysTryTypes: true, + }, + }, + }, + processor: angular.processInlineTemplates, + rules: { + ...rxjs.configs.recommended.rules, + "rxjs-angular/prefer-takeuntil": ["error", { alias: ["takeUntilDestroyed"] }], + "rxjs/no-exposed-subjects": ["error", { allowProtected: true }], + + // TODO: Enable these. + "@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, + "@bitwarden/platform/required-using": "error", + "@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }], + "@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }], + "@typescript-eslint/no-this-alias": ["error", { allowedNames: ["self"] }], + "@typescript-eslint/no-unused-expressions": ["error", { allowTernary: true }], + "@typescript-eslint/no-unused-vars": ["error", { args: "none" }], + + curly: ["error", "all"], + "no-console": "error", + + "import/order": [ + "error", + { + alphabetize: { + order: "asc", + }, + "newlines-between": "always", + pathGroups: [ + { + pattern: "@bitwarden/**", + group: "external", + position: "after", + }, + { + pattern: "src/**/*", + group: "parent", + position: "before", + }, + ], + pathGroupsExcludedImportTypes: ["builtin"], + }, + ], + "import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway + "import/no-restricted-paths": [ + "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/**/*", + from: [ + // Angular + "./libs/angular/**/*", + "./node_modules/@angular*/**/*", + + // Node + "./libs/node/**/*", + + //Generator + "./libs/tools/generator/components/**/*", + "./libs/tools/generator/core/**/*", + "./libs/tools/generator/extensions/**/*", + + // Import/export + "./libs/importer/**/*", + "./libs/tools/export/vault-export/vault-export-core/**/*", + ], + }, + { + // avoid import of unexported state objects + target: [ + "!(libs)/**/*", + "libs/!(common)/**/*", + "libs/common/!(src)/**/*", + "libs/common/src/!(platform)/**/*", + "libs/common/src/platform/!(state)/**/*", + ], + from: ["./libs/common/src/platform/state/**/*"], + // allow module index import + except: ["**/state/index.ts"], + }, + ], + }, + ], + "import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package., + }, + }, + { + // Everything in this config object targets our HTML files (external templates, + // and inline templates as long as we have the `processor` set on our TypeScript config above) + files: ["**/*.html"], + extends: [ + // Apply the recommended Angular template rules + // ...angular.configs.templateRecommended, + // Apply the Angular template rules which focus on accessibility of our apps + // ...angular.configs.templateAccessibility, + ], + languageOptions: { + parser: angular.templateParser, + }, + plugins: { + "@angular-eslint/template": angular.templatePlugin, + tailwindcss: eslintPluginTailwindCSS, + }, + rules: { + "@angular-eslint/template/button-has-type": "error", + "tailwindcss/no-custom-classname": [ + "error", + { + // uses negative lookahead to whitelist any class that doesn't start with "tw-" + // in other words: classnames that start with tw- must be valid TailwindCSS classes + whitelist: ["(?!(tw)\\-).*"], + }, + ], + "tailwindcss/enforces-negative-arbitrary-values": "error", + "tailwindcss/enforces-shorthand": "error", + "tailwindcss/no-contradicting-classname": "error", + }, + }, + + // Global quirks + { + files: ["apps/browser/src/**/*.ts", "libs/**/*.ts"], + ignores: [ + "apps/browser/src/autofill/{deprecated/content,content,notification}/**/*.ts", + "apps/browser/src/**/background/**/*.ts", // It's okay to have long lived listeners in the background + "apps/browser/src/platform/background.ts", + ], + rules: { + "no-restricted-syntax": [ + "error", + { + message: + "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead", + // This selector covers events like chrome.storage.onChange & chrome.runtime.onMessage + selector: + "CallExpression > [object.object.object.name='chrome'][property.name='addListener']", + }, + { + message: + "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead", + // This selector covers events like chrome.storage.local.onChange + selector: + "CallExpression > [object.object.object.object.name='chrome'][property.name='addListener']", + }, + ], + }, + }, + { + files: ["**/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports(), + }, + }, + + // App overrides. Be considerate if you override these. + { + files: ["apps/browser/src/**/*.ts"], + ignores: [ + "apps/browser/src/**/{content,popup,spec}/**/*.ts", + "apps/browser/src/**/autofill/{notification,overlay}/**/*.ts", + "apps/browser/src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts", + "apps/browser/src/**/*.spec.ts", + ], + rules: { + "no-restricted-globals": [ + "error", + { + name: "window", + message: + "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead.", + }, + ], + }, + }, + { + files: ["bitwarden_license/bit-common/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports(["@bitwarden/bit-common/*"]), + }, + }, + { + files: ["apps/**/*.ts"], + rules: { + // Catches static imports + "no-restricted-imports": buildNoRestrictedImports([ + "bitwarden_license/**", + "@bitwarden/bit-common/*", + "@bitwarden/bit-web/*", + ]), + }, + }, + { + files: ["apps/web/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + "bitwarden_license/**", + "@bitwarden/bit-common/*", + "@bitwarden/bit-web/*", + + "**/app/core/*", + "**/reports/*", + "**/app/shared/*", + "**/organizations/settings/*", + "**/organizations/policies/*", + ]), + }, + }, + + /// Team overrides + { + files: ["**/src/platform/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([], true), + }, + }, + { + files: [ + "apps/cli/src/admin-console/**/*.ts", + "apps/web/src/app/admin-console/**/*.ts", + "bitwarden_license/bit-cli/src/admin-console/**/*.ts", + "bitwarden_license/bit-web/src/app/admin-console/**/*.ts", + "libs/admin-console/src/**/*.ts", + ], + rules: { + "@angular-eslint/component-class-suffix": "error", + "@angular-eslint/contextual-lifecycle": "error", + "@angular-eslint/directive-class-suffix": "error", + "@angular-eslint/no-empty-lifecycle-method": "error", + "@angular-eslint/no-input-rename": "error", + "@angular-eslint/no-inputs-metadata-property": "error", + "@angular-eslint/no-output-native": "error", + "@angular-eslint/no-output-on-prefix": "error", + "@angular-eslint/no-output-rename": "error", + "@angular-eslint/no-outputs-metadata-property": "error", + "@angular-eslint/use-lifecycle-interface": "error", + "@angular-eslint/use-pipe-transform-interface": "error", + }, + }, + { + files: ["libs/common/src/state-migrations/**/*.ts"], + rules: { + "import/no-restricted-paths": [ + "error", + { + basePath: "libs/common/src/state-migrations", + zones: [ + { + target: "./", + from: "../", + // Relative to from, not basePath + except: ["state-migrations"], + message: + "State migrations should rarely import from the greater codebase. If you need to import from another location, take into account the likelihood of change in that code and consider copying to the migration instead.", + }, + ], + }, + ], + }, + }, + + // Keep ignores at the end + { + ignores: [ + "**/build/", + "**/dist/", + "**/coverage/", + ".angular/", + "storybook-static/", + + "**/node_modules/", + + "**/webpack.*.js", + "**/jest.config.js", + + "apps/browser/config/config.js", + "apps/browser/src/auth/scripts/duo.js", + "apps/browser/webpack/manifest.js", + + "apps/desktop/desktop_native", + "apps/desktop/src/auth/scripts/duo.js", + + "apps/web/config.js", + "apps/web/scripts/*.js", + "apps/web/tailwind.config.js", + + "apps/cli/config/config.js", + + "tailwind.config.js", + "libs/components/tailwind.config.base.js", + "libs/components/tailwind.config.js", + + "scripts/*.js", + ], + }, +); + +/** + * // Helper function for building no-restricted-imports rule + * @param {string[]} additionalForbiddenPatterns + * @returns {any} + */ +function buildNoRestrictedImports(additionalForbiddenPatterns = [], skipPlatform = false) { + return [ + "error", + { + patterns: [ + ...(skipPlatform ? [] : ["**/platform/**/internal", "**/platform/messaging/**"]), + "**/src/**/*", // Prevent relative imports across libs. + ].concat(additionalForbiddenPatterns), + }, + ]; +} diff --git a/jest.config.js b/jest.config.js index 3ed082bcbc30..e8815f92ffb1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -30,6 +30,7 @@ module.exports = { "/libs/billing/jest.config.js", "/libs/common/jest.config.js", "/libs/components/jest.config.js", + "/libs/eslint/jest.config.js", "/libs/tools/export/vault-export/vault-export-core/jest.config.js", "/libs/tools/generator/core/jest.config.js", "/libs/tools/generator/components/jest.config.js", @@ -42,6 +43,7 @@ module.exports = { "/libs/node/jest.config.js", "/libs/vault/jest.config.js", "/libs/key-management/jest.config.js", + "/libs/key-management-ui/jest.config.js", ], // Workaround for a memory leak that crashes tests in CI: diff --git a/libs/admin-console/.eslintrc.json b/libs/admin-console/.eslintrc.json deleted file mode 100644 index d8aa8f64a88a..000000000000 --- a/libs/admin-console/.eslintrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "overrides": [ - { - "files": ["*.ts"], - "extends": ["plugin:@angular-eslint/recommended"], - "rules": { - "@angular-eslint/component-class-suffix": "error", - "@angular-eslint/contextual-lifecycle": "error", - "@angular-eslint/directive-class-suffix": "error", - "@angular-eslint/no-empty-lifecycle-method": "error", - "@angular-eslint/no-input-rename": "error", - "@angular-eslint/no-inputs-metadata-property": "error", - "@angular-eslint/no-output-native": "error", - "@angular-eslint/no-output-on-prefix": "error", - "@angular-eslint/no-output-rename": "error", - "@angular-eslint/no-outputs-metadata-property": "error", - "@angular-eslint/use-lifecycle-interface": "error", - "@angular-eslint/use-pipe-transform-interface": "error" - } - } - ] -} diff --git a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts index 6aafbaf46787..890353d90394 100644 --- a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { KeyService } from "@bitwarden/key-management"; diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts index a230a20b2e37..7fe81ade4d20 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.ts b/libs/admin-console/src/common/collections/services/default-collection.service.ts index 4070c92f27c6..da50a25886e6 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.ts @@ -3,7 +3,7 @@ import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { Jsonify } from "type-fest"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts index 048a47339481..9700fcb695a6 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { first, firstValueFrom, of, ReplaySubject, takeWhile } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts index 2d5a083592bf..0ef8ae99ab38 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { combineLatest, filter, firstValueFrom, map } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider, DerivedState } from "@bitwarden/common/platform/state"; diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 0b19935985a7..52a22ac29464 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -63,7 +63,15 @@ export class CollectionsComponent implements OnInit { } if (this.organization == null) { - this.organization = await this.organizationService.get(this.cipher.organizationId); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(activeUserId) + .pipe( + map((organizations) => + organizations.find((org) => org.id === this.cipher.organizationId), + ), + ), + ); } } diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts b/libs/angular/src/auth/components/authentication-timeout.component.ts similarity index 89% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts rename to libs/angular/src/auth/components/authentication-timeout.component.ts index faa08cf073b4..1a5d398a2913 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts +++ b/libs/angular/src/auth/components/authentication-timeout.component.ts @@ -10,7 +10,7 @@ import { ButtonModule } from "@bitwarden/components"; * It provides a button to navigate to the login page. */ @Component({ - selector: "app-two-factor-expired", + selector: "app-authentication-timeout", standalone: true, imports: [CommonModule, JslibModule, ButtonModule, RouterModule], template: ` @@ -22,4 +22,4 @@ import { ButtonModule } from "@bitwarden/components"; `, }) -export class TwoFactorTimeoutComponent {} +export class AuthenticationTimeoutComponent {} diff --git a/libs/angular/src/auth/components/base-login-decryption-options-v1.component.ts b/libs/angular/src/auth/components/base-login-decryption-options-v1.component.ts index ca3906cead31..32396c878d9a 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options-v1.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options-v1.component.ts @@ -195,7 +195,7 @@ export class BaseLoginDecryptionOptionsComponentV1 implements OnInit, OnDestroy async loadNewUserData() { const autoEnrollStatus$ = defer(() => - this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(), + this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId), ).pipe( switchMap((organizationIdentifier) => { if (organizationIdentifier == undefined) { diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 7f54f35cb2a6..ea2f9695768c 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -10,12 +10,10 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { PasswordColorText } from "../../tools/password-strength/password-strength.component"; @@ -41,10 +39,8 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected i18nService: I18nService, protected keyService: KeyService, protected messagingService: MessagingService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, - protected stateService: StateService, protected dialogService: DialogService, protected kdfConfigService: KdfConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, diff --git a/libs/angular/src/auth/components/login-v1.component.ts b/libs/angular/src/auth/components/login-v1.component.ts index ffe1dda3aedd..3416901da97f 100644 --- a/libs/angular/src/auth/components/login-v1.component.ts +++ b/libs/angular/src/auth/components/login-v1.component.ts @@ -10,11 +10,9 @@ import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, PasswordLoginCredentials, - 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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -56,7 +54,7 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni return this.formGroup.controls.email; } - formGroup = this.formBuilder.group({ + formGroup = this.formBuilder.nonNullable.group({ email: ["", [Validators.required, Validators.email]], masterPassword: [ "", @@ -67,14 +65,12 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni protected twoFactorRoute = "2fa"; protected successRoute = "vault"; - // TODO: remove when email verification flag is removed - protected registerRoute$ = this.registerRouteService.registerRoute$(); protected forcePasswordResetRoute = "update-temp-password"; protected destroy$ = new Subject(); get loggedEmail() { - return this.formGroup.value.email; + return this.formGroup.controls.email.value; } constructor( @@ -95,8 +91,6 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni protected route: ActivatedRoute, protected loginEmailService: LoginEmailServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, - protected webAuthnLoginService: WebAuthnLoginServiceAbstraction, - protected registerRouteService: RegisterRouteService, protected toastService: ToastService, ) { super(environmentService, i18nService, platformUtilsService, toastService); @@ -146,8 +140,6 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni } async submit(showToast = true) { - const data = this.formGroup.value; - await this.setupCaptcha(); this.formGroup.markAllAsTouched(); @@ -170,10 +162,10 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni try { const credentials = new PasswordLoginCredentials( - data.email, - data.masterPassword, + this.formGroup.controls.email.value, + this.formGroup.controls.masterPassword.value, this.captchaToken, - null, + undefined, ); this.formPromise = this.loginStrategyService.logIn(credentials); diff --git a/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts b/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts index 386068ff7832..7409acf68458 100644 --- a/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts @@ -64,11 +64,12 @@ export class LoginViaAuthRequestComponentV1 protected StateEnum = State; protected state = State.StandardAuthRequest; - + protected webVaultUrl: string; protected twoFactorRoute = "2fa"; protected successRoute = "vault"; protected forcePasswordResetRoute = "update-temp-password"; private resendTimeout = 12000; + protected deviceManagementUrl: string; private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }; @@ -95,6 +96,12 @@ export class LoginViaAuthRequestComponentV1 ) { super(environmentService, i18nService, platformUtilsService, toastService); + // Get the web vault URL from the environment service + environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => { + this.webVaultUrl = env.getWebVaultUrl(); + this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`; + }); + // Gets signalR push notification // Only fires on approval to prevent enumeration this.authRequestService.authRequestPushNotification$ diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 279294f4c066..e4787aa8c016 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -15,10 +15,8 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi 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 { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; import { @@ -91,9 +89,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn i18nService: I18nService, protected keyService: KeyService, protected apiService: ApiService, - protected stateService: StateService, platformUtilsService: PlatformUtilsService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, environmentService: EnvironmentService, protected logService: LogService, protected auditService: AuditService, diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index 166707a19eab..de079a7ebca7 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -21,12 +21,11 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -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"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -34,7 +33,6 @@ import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -49,7 +47,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements resetPasswordAutoEnroll = false; onSuccessfulChangePassword: () => Promise; successRoute = "vault"; - userId: UserId; + activeUserId: UserId; forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None; ForceSetPasswordReason = ForceSetPasswordReason; @@ -60,7 +58,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements i18nService: I18nService, keyService: KeyService, messagingService: MessagingService, - passwordGenerationService: PasswordGenerationServiceAbstraction, platformUtilsService: PlatformUtilsService, private policyApiService: PolicyApiServiceAbstraction, policyService: PolicyService, @@ -68,7 +65,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements private apiService: ApiService, private syncService: SyncService, private route: ActivatedRoute, - stateService: StateService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserApiService: OrganizationUserApiService, private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, @@ -82,10 +78,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, @@ -102,10 +96,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements await this.syncService.fullSync(true); this.syncLoading = false; - this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; this.forceSetPasswordReason = await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(this.userId), + this.masterPasswordService.forceSetPasswordReason$(this.activeUserId), ); this.route.queryParams @@ -117,7 +111,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements } else { // Try to get orgSsoId from state as fallback // Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario. - return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(); + return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId); } }), filter((orgSsoId) => orgSsoId != null), @@ -173,10 +167,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements // in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one const existingUserPrivateKey = (await firstValueFrom( - this.keyService.userPrivateKey$(this.userId), + this.keyService.userPrivateKey$(this.activeUserId), )) as Uint8Array; const existingUserPublicKey = await firstValueFrom( - this.keyService.userPublicKey$(this.userId), + this.keyService.userPublicKey$(this.activeUserId), ); if (existingUserPrivateKey != null && existingUserPublicKey != null) { const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey); @@ -223,7 +217,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( this.orgId, - this.userId, + this.activeUserId, resetRequest, ); }); @@ -266,7 +260,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements // Clear force set password reason to allow navigation back to vault. await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.None, - this.userId, + this.activeUserId, ); // User now has a password so update account decryption options in state @@ -275,9 +269,9 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements ); userDecryptionOpts.hasMasterPassword = true; await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); - await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig); - await this.masterPasswordService.setMasterKey(masterKey, this.userId); - await this.keyService.setUserKey(userKey[0], this.userId); + await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig); + await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId); + await this.keyService.setUserKey(userKey[0], this.activeUserId); // Set private key only for new JIT provisioned users in MP encryption orgs // Existing TDE users will have private key set on sync or on login @@ -286,7 +280,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements this.forceSetPasswordReason != ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ) { - await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.userId); + await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId); } const localMasterKeyHash = await this.keyService.hashMasterKey( @@ -294,6 +288,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements masterKey, HashPurpose.LocalAuthorization, ); - await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId); } } diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 8b4df78175fb..5f5e53d8efe0 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -19,7 +19,6 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; 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"; @@ -50,7 +49,7 @@ export class SsoComponent implements OnInit { protected twoFactorRoute = "2fa"; protected successRoute = "lock"; protected trustedDeviceEncRoute = "login-initiated"; - protected changePasswordRoute = "set-password"; + protected changePasswordRoute = "set-password-jit"; protected forcePasswordResetRoute = "update-temp-password"; protected clientId: string; protected redirectUri: string; @@ -227,7 +226,8 @@ export class SsoComponent implements OnInit { // - TDE login decryption options component // - Browser SSO on extension open // Note: you cannot set this in state before 2FA b/c there won't be an account in state. - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId); // Users enrolled in admin acct recovery can be forced to set a new password after // having the admin set a temp password for them (affects TDE & standard users) @@ -339,14 +339,6 @@ export class SsoComponent implements OnInit { } private async handleChangePasswordRequired(orgIdentifier: string) { - const emailVerification = await this.configService.getFeatureFlag( - FeatureFlag.EmailVerification, - ); - - if (emailVerification) { - this.changePasswordRoute = "set-password-jit"; - } - await this.navigateViaCallbackOrRoute( this.onSuccessfulLoginChangePasswordNavigate, [this.changePasswordRoute], diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html index 8462a18ac2ef..087ecd2764e9 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html @@ -69,7 +69,7 @@
diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts index 6aca189a79ed..3e59e4a29b93 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts @@ -214,7 +214,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements } } - async selectOtherTwofactorMethod() { + async selectOtherTwoFactorMethod() { const dialogRef = TwoFactorOptionsComponent.open(this.dialogService); const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed); if (response.result === TwoFactorOptionsDialogResult.Provider) { @@ -262,7 +262,8 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements // Save off the OrgSsoIdentifier for use in the TDE flows // - TDE login decryption options component // - Browser SSO on extension open - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier, userId); this.loginEmailService.clearValues(); // note: this flow affects both TDE & standard users diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 5a1903d6671a..414aa1dc2a31 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -86,12 +86,12 @@ describe("TwoFactorComponent", () => { }; let selectedUserDecryptionOptions: BehaviorSubject; - let twoFactorTimeoutSubject: BehaviorSubject; + let authenticationSessionTimeoutSubject: BehaviorSubject; beforeEach(() => { - twoFactorTimeoutSubject = new BehaviorSubject(false); + authenticationSessionTimeoutSubject = new BehaviorSubject(false); mockLoginStrategyService = mock(); - mockLoginStrategyService.twoFactorTimeout$ = twoFactorTimeoutSubject; + mockLoginStrategyService.authenticationSessionTimeout$ = authenticationSessionTimeoutSubject; mockRouter = mock(); mockI18nService = mock(); mockApiService = mock(); @@ -153,7 +153,9 @@ describe("TwoFactorComponent", () => { }), }; - selectedUserDecryptionOptions = new BehaviorSubject(null); + selectedUserDecryptionOptions = new BehaviorSubject( + mockUserDecryptionOpts.withMasterPassword, + ); mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions; TestBed.configureTestingModule({ @@ -497,8 +499,8 @@ describe("TwoFactorComponent", () => { }); it("navigates to the timeout route when timeout expires", async () => { - twoFactorTimeoutSubject.next(true); + authenticationSessionTimeoutSubject.next(true); - expect(mockRouter.navigate).toHaveBeenCalledWith(["2fa-timeout"]); + expect(mockRouter.navigate).toHaveBeenCalledWith(["authentication-timeout"]); }); }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index e2b41ad086d6..e43797332ec1 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -71,7 +71,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected changePasswordRoute = "set-password"; protected forcePasswordResetRoute = "update-temp-password"; protected successRoute = "vault"; - protected twoFactorTimeoutRoute = "2fa-timeout"; + protected twoFactorTimeoutRoute = "authentication-timeout"; get isDuoProvider(): boolean { return ( @@ -102,10 +102,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected toastService: ToastService, ) { super(environmentService, i18nService, platformUtilsService, toastService); + this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); - // Add subscription to twoFactorTimeout$ and navigate to twoFactorTimeoutRoute if expired - this.loginStrategyService.twoFactorTimeout$ + // Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired + this.loginStrategyService.authenticationSessionTimeout$ .pipe(takeUntilDestroyed()) .subscribe(async (expired) => { if (!expired) { @@ -287,7 +288,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI // Save off the OrgSsoIdentifier for use in the TDE flows // - TDE login decryption options component // - Browser SSO on extension open - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier, userId); this.loginEmailService.clearValues(); // note: this flow affects both TDE & standard users diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 1d1057d9aa67..e6cefd40d1d8 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -16,11 +16,9 @@ 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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -39,12 +37,10 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { protected router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, policyService: PolicyService, keyService: KeyService, messagingService: MessagingService, private apiService: ApiService, - stateService: StateService, private userVerificationService: UserVerificationService, private logService: LogService, dialogService: DialogService, @@ -57,10 +53,8 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 3fb1f7400ec2..95c56d084867 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -20,12 +20,10 @@ 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 { 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, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -51,12 +49,10 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp constructor( i18nService: I18nService, platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, policyService: PolicyService, keyService: KeyService, messagingService: MessagingService, private apiService: ApiService, - stateService: StateService, private syncService: SyncService, private logService: LogService, private userVerificationService: UserVerificationService, @@ -71,10 +67,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, diff --git a/libs/angular/src/auth/guards/active-auth.guard.spec.ts b/libs/angular/src/auth/guards/active-auth.guard.spec.ts new file mode 100644 index 000000000000..c3417b9d41d0 --- /dev/null +++ b/libs/angular/src/auth/guards/active-auth.guard.spec.ts @@ -0,0 +1,71 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { activeAuthGuard } from "./active-auth.guard"; + +@Component({ template: "" }) +class EmptyComponent {} + +describe("activeAuthGuard", () => { + const setup = (authType: AuthenticationType | null) => { + const loginStrategyService: MockProxy = + mock(); + const currentAuthTypeSubject = new BehaviorSubject(authType); + loginStrategyService.currentAuthType$ = currentAuthTypeSubject; + + const logService: MockProxy = mock(); + + const testBed = TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: "", component: EmptyComponent }, + { + path: "protected-route", + component: EmptyComponent, + canActivate: [activeAuthGuard()], + }, + { path: "login", component: EmptyComponent }, + ]), + ], + providers: [ + { provide: LoginStrategyServiceAbstraction, useValue: loginStrategyService }, + { provide: LogService, useValue: logService }, + ], + declarations: [EmptyComponent], + }); + + return { + router: testBed.inject(Router), + logService, + loginStrategyService, + }; + }; + + it("creates the guard", () => { + const { router } = setup(AuthenticationType.Password); + expect(router).toBeTruthy(); + }); + + it("allows access with an active login session", async () => { + const { router } = setup(AuthenticationType.Password); + + await router.navigate(["protected-route"]); + expect(router.url).toBe("/protected-route"); + }); + + it("redirects to login with no active session", async () => { + const { router, logService } = setup(null); + + await router.navigate(["protected-route"]); + expect(router.url).toBe("/login"); + expect(logService.error).toHaveBeenCalledWith("No active login session found."); + }); +}); diff --git a/libs/angular/src/auth/guards/active-auth.guard.ts b/libs/angular/src/auth/guards/active-auth.guard.ts new file mode 100644 index 000000000000..56213bbd9792 --- /dev/null +++ b/libs/angular/src/auth/guards/active-auth.guard.ts @@ -0,0 +1,28 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +/** + * Guard that ensures there is an active login session before allowing access + * to the new device verification route. + * If not, redirects to login. + */ +export function activeAuthGuard(): CanActivateFn { + return async () => { + const loginStrategyService = inject(LoginStrategyServiceAbstraction); + const logService = inject(LogService); + const router = inject(Router); + + // Check if we have a valid login session + const authType = await firstValueFrom(loginStrategyService.currentAuthType$); + if (authType === null) { + logService.error("No active login session found."); + return router.createUrlTree(["/login"]); + } + + return true; + }; +} diff --git a/libs/angular/src/auth/guards/index.ts b/libs/angular/src/auth/guards/index.ts index 1760a870b3ad..026848c4b08d 100644 --- a/libs/angular/src/auth/guards/index.ts +++ b/libs/angular/src/auth/guards/index.ts @@ -1,4 +1,5 @@ export * from "./auth.guard"; +export * from "./active-auth.guard"; export * from "./lock.guard"; export * from "./redirect.guard"; export * from "./tde-decryption-required.guard"; diff --git a/libs/angular/src/auth/guards/unauth.guard.spec.ts b/libs/angular/src/auth/guards/unauth.guard.spec.ts index 6d8619f4d434..ec36b146a037 100644 --- a/libs/angular/src/auth/guards/unauth.guard.spec.ts +++ b/libs/angular/src/auth/guards/unauth.guard.spec.ts @@ -5,17 +5,48 @@ import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec"; +import { Account, AccountService } 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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { unauthGuardFn } from "./unauth.guard"; describe("UnauthGuard", () => { - const setup = (authStatus: AuthenticationStatus) => { + const activeUser: Account = { + id: "fake_user_id" as UserId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }; + + const setup = ( + activeUser: Account | null, + authStatus: AuthenticationStatus | null = null, + tdeEnabled: boolean = false, + everHadUserKey: boolean = false, + ) => { + const accountService: MockProxy = mock(); const authService: MockProxy = mock(); - authService.getAuthStatus.mockResolvedValue(authStatus); - const activeAccountStatusObservable = new BehaviorSubject(authStatus); - authService.activeAccountStatus$ = activeAccountStatusObservable; + const keyService: MockProxy = mock(); + const deviceTrustService: MockProxy = + mock(); + const logService: MockProxy = mock(); + + accountService.activeAccount$ = new BehaviorSubject(activeUser); + + if (authStatus !== null) { + const activeAccountStatusObservable = new BehaviorSubject(authStatus); + authService.authStatusFor$.mockReturnValue(activeAccountStatusObservable); + } + + keyService.everHadUserKey$ = new BehaviorSubject(everHadUserKey); + deviceTrustService.supportsDeviceTrustByUserId$.mockReturnValue( + new BehaviorSubject(tdeEnabled), + ); const testBed = TestBed.configureTestingModule({ imports: [ @@ -30,6 +61,7 @@ describe("UnauthGuard", () => { { path: "lock", component: EmptyComponent }, { path: "testhomepage", component: EmptyComponent }, { path: "testlocked", component: EmptyComponent }, + { path: "login-initiated", component: EmptyComponent }, { path: "testOverrides", component: EmptyComponent, @@ -39,7 +71,13 @@ describe("UnauthGuard", () => { }, ]), ], - providers: [{ provide: AuthService, useValue: authService }], + providers: [ + { provide: AccountService, useValue: accountService }, + { provide: AuthService, useValue: authService }, + { provide: KeyService, useValue: keyService }, + { provide: DeviceTrustServiceAbstraction, useValue: deviceTrustService }, + { provide: LogService, useValue: logService }, + ], }); return { @@ -48,40 +86,54 @@ describe("UnauthGuard", () => { }; it("should be created", () => { - const { router } = setup(AuthenticationStatus.LoggedOut); + const { router } = setup(null, AuthenticationStatus.LoggedOut); expect(router).toBeTruthy(); }); it("should redirect to /vault for guarded routes when logged in and unlocked", async () => { - const { router } = setup(AuthenticationStatus.Unlocked); + const { router } = setup(activeUser, AuthenticationStatus.Unlocked); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/vault"); }); + it("should allow access to guarded routes when account is null", async () => { + const { router } = setup(null); + + await router.navigateByUrl("unauth-guarded-route"); + expect(router.url).toBe("/unauth-guarded-route"); + }); + it("should allow access to guarded routes when logged out", async () => { - const { router } = setup(AuthenticationStatus.LoggedOut); + const { router } = setup(null, AuthenticationStatus.LoggedOut); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/unauth-guarded-route"); }); + it("should redirect to /login-initiated when locked, TDE is enabled, and the user hasn't decrypted yet", async () => { + const { router } = setup(activeUser, AuthenticationStatus.Locked, true, false); + + await router.navigateByUrl("unauth-guarded-route"); + expect(router.url).toBe("/login-initiated"); + }); + it("should redirect to /lock for guarded routes when locked", async () => { - const { router } = setup(AuthenticationStatus.Locked); + const { router } = setup(activeUser, AuthenticationStatus.Locked); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/lock"); }); it("should redirect to /testhomepage for guarded routes when testOverrides are provided and the account is unlocked", async () => { - const { router } = setup(AuthenticationStatus.Unlocked); + const { router } = setup(activeUser, AuthenticationStatus.Unlocked); await router.navigateByUrl("testOverrides"); expect(router.url).toBe("/testhomepage"); }); it("should redirect to /testlocked for guarded routes when testOverrides are provided and the account is locked", async () => { - const { router } = setup(AuthenticationStatus.Locked); + const { router } = setup(activeUser, AuthenticationStatus.Locked); await router.navigateByUrl("testOverrides"); expect(router.url).toBe("/testlocked"); diff --git a/libs/angular/src/auth/guards/unauth.guard.ts b/libs/angular/src/auth/guards/unauth.guard.ts index f96668773efb..1ac0eebb458e 100644 --- a/libs/angular/src/auth/guards/unauth.guard.ts +++ b/libs/angular/src/auth/guards/unauth.guard.ts @@ -1,9 +1,13 @@ import { inject } from "@angular/core"; -import { CanActivateFn, Router, UrlTree } from "@angular/router"; -import { Observable, map } from "rxjs"; +import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { AccountService } 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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { KeyService } from "@bitwarden/key-management"; type UnauthRoutes = { homepage: () => string; @@ -15,23 +19,54 @@ const defaultRoutes: UnauthRoutes = { locked: "/lock", }; -function unauthGuard(routes: UnauthRoutes): Observable { +// TODO: PM-17195 - Investigate consolidating unauthGuard and redirectGuard into AuthStatusGuard +async function unauthGuard( + route: ActivatedRouteSnapshot, + routes: UnauthRoutes, +): Promise { + const accountService = inject(AccountService); const authService = inject(AuthService); const router = inject(Router); + const keyService = inject(KeyService); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); + const logService = inject(LogService); - return authService.activeAccountStatus$.pipe( - map((status) => { - if (status == null || status === AuthenticationStatus.LoggedOut) { - return true; - } else if (status === AuthenticationStatus.Locked) { - return router.createUrlTree([routes.locked]); - } else { - return router.createUrlTree([routes.homepage()]); - } - }), + const activeUser = await firstValueFrom(accountService.activeAccount$); + + if (!activeUser) { + return true; + } + + const authStatus = await firstValueFrom(authService.authStatusFor$(activeUser.id)); + + if (authStatus == null || authStatus === AuthenticationStatus.LoggedOut) { + return true; + } + + if (authStatus === AuthenticationStatus.Unlocked) { + return router.createUrlTree([routes.homepage()]); + } + + const tdeEnabled = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(activeUser.id), ); + const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$); + + // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the + // login decryption options component. + if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { + logService.info( + "Sending user to TDE decryption options. AuthStatus is %s. TDE support is %s. Ever had user key is %s.", + AuthenticationStatus[authStatus], + tdeEnabled, + everHadUserKey, + ); + return router.createUrlTree(["/login-initiated"]); + } + + return router.createUrlTree([routes.locked]); } export function unauthGuardFn(overrides: Partial = {}): CanActivateFn { - return () => unauthGuard({ ...defaultRoutes, ...overrides }); + return async (route) => unauthGuard(route, { ...defaultRoutes, ...overrides }); } diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts index cebd81846c16..edb233cc76e1 100644 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts @@ -3,14 +3,14 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountService, AccountInfo } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -105,7 +105,16 @@ export class AddAccountCreditDialogComponent implements OnInit { this.formGroup.patchValue({ creditAmount: 20.0, }); - this.organization = await this.organizationService.get(this.dialogParams.organizationId); + this.user = await firstValueFrom(this.accountService.activeAccount$); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(this.user.id) + .pipe( + map((organizations) => + organizations.find((org) => org.id === this.dialogParams.organizationId), + ), + ), + ); payPalCustomField = "organization_id:" + this.organization.id; this.payPalConfig.subject = this.organization.name; } else if (this.dialogParams.providerId) { @@ -119,7 +128,6 @@ export class AddAccountCreditDialogComponent implements OnInit { this.formGroup.patchValue({ creditAmount: 10.0, }); - this.user = await firstValueFrom(this.accountService.activeAccount$); payPalCustomField = "user_id:" + this.user.id; this.payPalConfig.subject = this.user.email; } diff --git a/libs/angular/src/billing/components/index.ts b/libs/angular/src/billing/components/index.ts index 675d7555ed25..dacb5b265bdd 100644 --- a/libs/angular/src/billing/components/index.ts +++ b/libs/angular/src/billing/components/index.ts @@ -2,3 +2,4 @@ export * from "./add-account-credit-dialog/add-account-credit-dialog.component"; export * from "./invoices/invoices.component"; export * from "./invoices/no-invoices.component"; export * from "./manage-tax-information/manage-tax-information.component"; +export * from "./premium.component"; diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts index 13a6d2d0cc37..885afb1ae676 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts @@ -72,6 +72,10 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { } } + markAllAsTouched() { + this.formGroup.markAllAsTouched(); + } + async ngOnInit() { if (this.startWith) { this.formGroup.controls.country.setValue(this.startWith.country); diff --git a/libs/angular/src/vault/components/premium.component.ts b/libs/angular/src/billing/components/premium.component.ts similarity index 76% rename from libs/angular/src/vault/components/premium.component.ts rename to libs/angular/src/billing/components/premium.component.ts index 8b1f215ef42d..6d0b90385bab 100644 --- a/libs/angular/src/vault/components/premium.component.ts +++ b/libs/angular/src/billing/components/premium.component.ts @@ -6,13 +6,11 @@ import { firstValueFrom, Observable, switchMap } from "rxjs"; 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 { 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 { 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, SimpleDialogOptions } from "@bitwarden/components"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; @Directive() export class PremiumComponent implements OnInit { @@ -20,17 +18,16 @@ export class PremiumComponent implements OnInit { price = 10; refreshPromise: Promise; cloudWebVaultUrl: string; - extensionRefreshFlagEnabled: boolean; constructor( protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService, - protected configService: ConfigService, private logService: LogService, protected dialogService: DialogService, private environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, accountService: AccountService, ) { this.isPremium$ = accountService.activeAccount$.pipe( @@ -42,16 +39,17 @@ export class PremiumComponent implements OnInit { async ngOnInit() { this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); - this.extensionRefreshFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); } async refresh() { try { this.refreshPromise = this.apiService.refreshIdentityToken(); await this.refreshPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("refreshComplete")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("refreshComplete"), + }); } catch (e) { this.logService.error(e); } @@ -61,15 +59,13 @@ export class PremiumComponent implements OnInit { const dialogOpts: SimpleDialogOptions = { title: { key: "continueToBitwardenDotCom" }, content: { - key: this.extensionRefreshFlagEnabled ? "premiumPurchaseAlertV2" : "premiumPurchaseAlert", + key: "premiumPurchaseAlertV2", }, type: "info", }; - if (this.extensionRefreshFlagEnabled) { - dialogOpts.acceptButtonText = { key: "continue" }; - dialogOpts.cancelButtonText = { key: "close" }; - } + dialogOpts.acceptButtonText = { key: "continue" }; + dialogOpts.cancelButtonText = { key: "close" }; const confirmed = await this.dialogService.openSimpleDialog(dialogOpts); diff --git a/libs/angular/src/directives/not-premium.directive.ts b/libs/angular/src/billing/directives/not-premium.directive.ts similarity index 100% rename from libs/angular/src/directives/not-premium.directive.ts rename to libs/angular/src/billing/directives/not-premium.directive.ts diff --git a/libs/angular/src/directives/premium.directive.ts b/libs/angular/src/billing/directives/premium.directive.ts similarity index 100% rename from libs/angular/src/directives/premium.directive.ts rename to libs/angular/src/billing/directives/premium.directive.ts diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 37dec53b9c7d..534a1337eda1 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -54,7 +54,11 @@ export class ShareComponent implements OnInit, OnDestroy { const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); - this.organizations$ = this.organizationService.memberOrganizations$.pipe( + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + + this.organizations$ = this.organizationService.memberOrganizations$(userId).pipe( map((orgs) => { return orgs .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 4f5a8f6673ce..6ef2cf1d4dac 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -25,22 +25,22 @@ import { TableModule, ToastModule, TypographyModule, + CopyClickDirective, + A11yTitleDirective, } from "@bitwarden/components"; import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component"; +import { NotPremiumDirective } from "./billing/directives/not-premium.directive"; import { DeprecatedCalloutComponent } from "./components/callout.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; -import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; import { BoxRowDirective } from "./directives/box-row.directive"; -import { CopyClickDirective } from "./directives/copy-click.directive"; import { CopyTextDirective } from "./directives/copy-text.directive"; import { FallbackSrcDirective } from "./directives/fallback-src.directive"; import { IfFeatureDirective } from "./directives/if-feature.directive"; import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive"; import { InputVerbatimDirective } from "./directives/input-verbatim.directive"; import { LaunchClickDirective } from "./directives/launch-click.directive"; -import { NotPremiumDirective } from "./directives/not-premium.directive"; import { StopClickDirective } from "./directives/stop-click.directive"; import { StopPropDirective } from "./directives/stop-prop.directive"; import { TextDragDirective } from "./directives/text-drag.directive"; @@ -83,10 +83,11 @@ import { IconComponent } from "./vault/components/icon.component"; LinkModule, IconModule, TextDragDirective, + CopyClickDirective, + A11yTitleDirective, ], declarations: [ A11yInvalidDirective, - A11yTitleDirective, ApiActionDirective, AutofocusDirective, BoxRowDirective, @@ -105,7 +106,6 @@ import { IconComponent } from "./vault/components/icon.component"; StopClickDirective, StopPropDirective, TrueFalseValueDirective, - CopyClickDirective, LaunchClickDirective, UserNamePipe, PasswordStrengthComponent, diff --git a/libs/angular/src/platform/pipes/i18n.pipe.ts b/libs/angular/src/platform/pipes/i18n.pipe.ts index 1f92bbb19a4d..a6fdbc78255f 100644 --- a/libs/angular/src/platform/pipes/i18n.pipe.ts +++ b/libs/angular/src/platform/pipes/i18n.pipe.ts @@ -2,6 +2,9 @@ import { Pipe, PipeTransform } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +/** + * @deprecated: Please use the I18nPipe from @bitwarden/ui-common + */ @Pipe({ name: "i18n", }) diff --git a/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index e7547f9b8283..82b5affcc22c 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -1,138 +1,4 @@ -import { Provider } from "@angular/core"; -import { Constructor, Opaque } from "type-fest"; - -import { SafeInjectionToken } from "../../services/injection-tokens"; - /** - * The return type of the {@link safeProvider} helper function. - * Used to distinguish a type safe provider definition from a non-type safe provider definition. + * @deprecated: Please use the SafeProvider & safeProvider from @bitwarden/ui-common */ -export type SafeProvider = Opaque; - -// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0 -type AbstractConstructor = abstract new (...args: any) => T; - -type MapParametersToDeps = { - [K in keyof T]: AbstractConstructor | SafeInjectionToken; -}; - -type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; - -/** - * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken - */ -type ProviderInstanceType = - T extends SafeInjectionToken - ? InstanceType> - : T extends Constructor | AbstractConstructor - ? InstanceType - : never; - -/** - * Represents a dependency provided with the useClass option. - */ -type SafeClassProvider< - A extends AbstractConstructor | SafeInjectionToken, - I extends Constructor>, - D extends MapParametersToDeps>, -> = { - provide: A; - useClass: I; - deps: D; -}; - -/** - * Represents a dependency provided with the useValue option. - */ -type SafeValueProvider, V extends SafeInjectionTokenType> = { - provide: A; - useValue: V; -}; - -/** - * Represents a dependency provided with the useFactory option. - */ -type SafeFactoryProvider< - A extends AbstractConstructor | SafeInjectionToken, - I extends (...args: any) => ProviderInstanceType, - D extends MapParametersToDeps>, -> = { - provide: A; - useFactory: I; - deps: D; - multi?: boolean; -}; - -/** - * Represents a dependency provided with the useExisting option. - */ -type SafeExistingProvider< - A extends Constructor | AbstractConstructor | SafeInjectionToken, - I extends Constructor> | AbstractConstructor>, -> = { - provide: A; - useExisting: I; -}; - -/** - * Represents a dependency where there is no abstract token, the token is the implementation - */ -type SafeConcreteProvider< - I extends Constructor, - D extends MapParametersToDeps>, -> = { - provide: I; - deps: D; -}; - -/** - * If useAngularDecorators: true is specified, do not require a deps array. - * This is a manual override for where @Injectable decorators are used - */ -type UseAngularDecorators = Omit & { - useAngularDecorators: true; -}; - -/** - * Represents a type with a deps array that may optionally be overridden with useAngularDecorators - */ -type AllowAngularDecorators = T | UseAngularDecorators; - -/** - * A factory function that creates a provider for the ngModule providers array. - * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. - * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, - * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. - * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) - * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) - * @returns The exact same object without modification (pass-through). - */ -export const safeProvider = < - // types for useClass - AClass extends AbstractConstructor | SafeInjectionToken, - IClass extends Constructor>, - DClass extends MapParametersToDeps>, - // types for useValue - AValue extends SafeInjectionToken, - VValue extends SafeInjectionTokenType, - // types for useFactory - AFactory extends AbstractConstructor | SafeInjectionToken, - IFactory extends (...args: any) => ProviderInstanceType, - DFactory extends MapParametersToDeps>, - // types for useExisting - AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, - IExisting extends - | Constructor> - | AbstractConstructor>, - // types for no token - IConcrete extends Constructor, - DConcrete extends MapParametersToDeps>, ->( - provider: - | AllowAngularDecorators> - | SafeValueProvider - | AllowAngularDecorators> - | SafeExistingProvider - | AllowAngularDecorators> - | Constructor, -): SafeProvider => provider as SafeProvider; +export { SafeProvider, safeProvider } from "@bitwarden/ui-common"; diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 3842c3250e14..2c740d5bb42d 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { InjectionToken } from "@angular/core"; import { Observable, Subject } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -14,17 +13,9 @@ import { Theme } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message } from "@bitwarden/common/platform/messaging"; import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; - -declare const tag: unique symbol; -/** - * A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter. - * @remarks The default angular implementation does not use the generic type to define the structure of the object, - * so the structural type system will not complain about a mismatch in the type parameter. - * This is solved by assigning T to an arbitrary private property. - */ -export class SafeInjectionToken extends InjectionToken { - private readonly [tag]: T; -} +import { SafeInjectionToken } from "@bitwarden/ui-common"; +// Re-export the SafeInjectionToken from ui-common +export { SafeInjectionToken } from "@bitwarden/ui-common"; export const WINDOW = new SafeInjectionToken("WINDOW"); export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken< diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 803808612cf6..719e3a084f1d 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -35,18 +35,18 @@ import { UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, LogoutReason, - RegisterRouteService, AuthRequestApiService, DefaultAuthRequestApiService, DefaultLoginSuccessHandlerService, LoginSuccessHandlerService, + PasswordLoginStrategy, + PasswordLoginStrategyData, LoginApprovalComponentServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; @@ -68,8 +68,8 @@ import { } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrgDomainApiService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain-api.service"; import { OrgDomainService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain.service"; import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/services/organization-management-preferences/default-organization-management-preferences.service"; @@ -147,13 +147,15 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { TaxService } from "@bitwarden/common/billing/services/tax.service"; +import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation"; +import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.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 { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService, RegionConfig, @@ -176,6 +178,16 @@ import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/inter import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; +// eslint-disable-next-line no-restricted-imports -- Needed for service creation +import { + DefaultNotificationsService, + NoopNotificationsService, + SignalRConnectionService, + UnsupportedWebPushConnectionService, + WebPushConnectionService, + WebPushNotificationsApiService, +} from "@bitwarden/common/platform/notifications/internal"; import { TaskSchedulerService, DefaultTaskSchedulerService, @@ -184,8 +196,6 @@ 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 { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; -import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service"; @@ -193,7 +203,6 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo 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 { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; @@ -227,7 +236,6 @@ import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -274,12 +282,6 @@ import { PasswordGenerationServiceAbstraction, UsernameGenerationServiceAbstraction, } from "@bitwarden/generator-legacy"; -import { - ImportApiService, - ImportApiServiceAbstraction, - ImportService, - ImportServiceAbstraction, -} from "@bitwarden/importer/core"; import { KeyService, DefaultKeyService, @@ -293,7 +295,8 @@ import { UserAsymmetricKeysRegenerationApiService, DefaultUserAsymmetricKeysRegenerationApiService, } from "@bitwarden/key-management"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { SafeInjectionToken } from "@bitwarden/ui-common"; +import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault"; import { VaultExportService, VaultExportServiceAbstraction, @@ -303,9 +306,6 @@ import { IndividualVaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { ViewCacheService } from "../platform/abstractions/view-cache.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; @@ -323,7 +323,6 @@ import { MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, - SafeInjectionToken, SECURE_STORAGE, STATE_FACTORY, SUPPORTS_SECURE_STORAGE, @@ -555,7 +554,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: InternalAccountService, useClass: AccountServiceImplementation, - deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider], + deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider, SingleUserStateProvider], }), safeProvider({ provide: AccountServiceAbstraction, @@ -797,7 +796,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: SsoLoginServiceAbstraction, useClass: SsoLoginService, - deps: [StateProvider], + deps: [StateProvider, LogService], }), safeProvider({ provide: STATE_FACTORY, @@ -818,26 +817,6 @@ const safeProviders: SafeProvider[] = [ MigrationRunner, ], }), - safeProvider({ - provide: ImportApiServiceAbstraction, - useClass: ImportApiService, - deps: [ApiServiceAbstraction], - }), - safeProvider({ - provide: ImportServiceAbstraction, - useClass: ImportService, - deps: [ - CipherServiceAbstraction, - FolderServiceAbstraction, - ImportApiServiceAbstraction, - I18nServiceAbstraction, - CollectionService, - KeyService, - EncryptService, - PinServiceAbstraction, - AccountServiceAbstraction, - ], - }), safeProvider({ provide: IndividualVaultExportServiceAbstraction, useClass: IndividualVaultExportService, @@ -878,19 +857,36 @@ const safeProviders: SafeProvider[] = [ deps: [LogService, I18nServiceAbstraction, StateProvider], }), safeProvider({ - provide: NotificationsServiceAbstraction, - useClass: devFlagEnabled("noopNotifications") ? NoopNotificationsService : NotificationsService, + provide: WebPushNotificationsApiService, + useClass: WebPushNotificationsApiService, + deps: [ApiServiceAbstraction, AppIdServiceAbstraction], + }), + safeProvider({ + provide: SignalRConnectionService, + useClass: SignalRConnectionService, + deps: [ApiServiceAbstraction, LogService], + }), + safeProvider({ + provide: WebPushConnectionService, + useClass: UnsupportedWebPushConnectionService, + deps: [], + }), + safeProvider({ + provide: NotificationsService, + useClass: devFlagEnabled("noopNotifications") + ? NoopNotificationsService + : DefaultNotificationsService, deps: [ LogService, SyncService, AppIdServiceAbstraction, - ApiServiceAbstraction, EnvironmentService, LOGOUT_CALLBACK, - StateServiceAbstraction, - AuthServiceAbstraction, MessagingServiceAbstraction, - TaskSchedulerService, + AccountServiceAbstraction, + SignalRConnectionService, + AuthServiceAbstraction, + WebPushConnectionService, ], }), safeProvider({ @@ -993,13 +989,14 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: InternalOrganizationServiceAbstraction, - useClass: OrganizationService, + useClass: DefaultOrganizationService, deps: [StateProvider], }), safeProvider({ provide: OrganizationServiceAbstraction, useExisting: InternalOrganizationServiceAbstraction, }), + safeProvider({ provide: OrganizationUserApiService, useClass: DefaultOrganizationUserApiService, @@ -1234,7 +1231,6 @@ const safeProviders: SafeProvider[] = [ deps: [ ApiServiceAbstraction, BillingApiServiceAbstraction, - ConfigService, KeyService, EncryptService, I18nServiceAbstraction, @@ -1354,11 +1350,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultServerSettingsService, deps: [ConfigService], }), - safeProvider({ - provide: RegisterRouteService, - useClass: RegisterRouteService, - deps: [ConfigService], - }), safeProvider({ provide: AnonLayoutWrapperDataService, useClass: DefaultAnonLayoutWrapperDataService, @@ -1400,7 +1391,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CipherAuthorizationService, useClass: DefaultCipherAuthorizationService, - deps: [CollectionService, OrganizationServiceAbstraction], + deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction], }), safeProvider({ provide: AuthRequestApiService, @@ -1441,6 +1432,37 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultLoginSuccessHandlerService, deps: [SyncService, UserAsymmetricKeysRegenerationService], }), + safeProvider({ + provide: PasswordLoginStrategy, + useClass: PasswordLoginStrategy, + deps: [ + PasswordLoginStrategyData, + PasswordStrengthServiceAbstraction, + PolicyServiceAbstraction, + LoginStrategyServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, + KeyService, + EncryptService, + ApiServiceAbstraction, + TokenServiceAbstraction, + AppIdServiceAbstraction, + PlatformUtilsServiceAbstraction, + MessagingServiceAbstraction, + LogService, + StateServiceAbstraction, + TwoFactorServiceAbstraction, + InternalUserDecryptionOptionsServiceAbstraction, + BillingAccountProfileStateService, + VaultTimeoutSettingsServiceAbstraction, + KdfConfigService, + ], + }), + safeProvider({ + provide: PasswordLoginStrategyData, + useClass: PasswordLoginStrategyData, + deps: [], + }), ]; @NgModule({ diff --git a/libs/angular/src/tools/generator/components/generator.component.ts b/libs/angular/src/tools/generator/components/generator.component.ts deleted file mode 100644 index 1f3c635e499c..000000000000 --- a/libs/angular/src/tools/generator/components/generator.component.ts +++ /dev/null @@ -1,389 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { BehaviorSubject, combineLatest, firstValueFrom, Subject } from "rxjs"; -import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators"; - -import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options"; -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 { ToastService } from "@bitwarden/components"; -import { - GeneratorType, - DefaultPasswordBoundaries as DefaultBoundaries, -} from "@bitwarden/generator-core"; -import { - PasswordGenerationServiceAbstraction, - UsernameGenerationServiceAbstraction, - UsernameGeneratorOptions, - PasswordGeneratorOptions, -} from "@bitwarden/generator-legacy"; - -export class EmailForwarderOptions { - name: string; - value: string; - validForSelfHosted: boolean; -} - -@Directive() -export class GeneratorComponent implements OnInit, OnDestroy { - @Input() comingFromAddEdit = false; - @Input() type: GeneratorType | ""; - @Output() onSelected = new EventEmitter(); - - usernameGeneratingPromise: Promise; - typeOptions: any[]; - usernameTypeOptions: any[]; - subaddressOptions: any[]; - catchallOptions: any[]; - forwardOptions: EmailForwarderOptions[]; - usernameOptions: UsernameGeneratorOptions = { website: null }; - passwordOptions: PasswordGeneratorOptions = {}; - username = "-"; - password = "-"; - showOptions = false; - avoidAmbiguous = false; - enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions; - usernameWebsite: string = null; - - get passTypeOptions() { - return this._passTypeOptions.filter((o) => !o.disabled); - } - private _passTypeOptions: { name: string; value: GeneratorType; disabled: boolean }[]; - - private destroy$ = new Subject(); - private isInitialized$ = new BehaviorSubject(false); - - // update screen reader minimum password length with 500ms debounce - // so that the user isn't flooded with status updates - private _passwordOptionsMinLengthForReader = new BehaviorSubject( - DefaultBoundaries.length.min, - ); - protected passwordOptionsMinLengthForReader$ = this._passwordOptionsMinLengthForReader.pipe( - map((val) => val || DefaultBoundaries.length.min), - debounceTime(500), - ); - - private _password = new BehaviorSubject("-"); - - constructor( - protected passwordGenerationService: PasswordGenerationServiceAbstraction, - protected usernameGenerationService: UsernameGenerationServiceAbstraction, - protected platformUtilsService: PlatformUtilsService, - protected accountService: AccountService, - protected i18nService: I18nService, - protected logService: LogService, - protected route: ActivatedRoute, - protected ngZone: NgZone, - private win: Window, - protected toastService: ToastService, - ) { - this.typeOptions = [ - { name: i18nService.t("password"), value: "password" }, - { name: i18nService.t("username"), value: "username" }, - ]; - this._passTypeOptions = [ - { name: i18nService.t("password"), value: "password", disabled: false }, - { name: i18nService.t("passphrase"), value: "passphrase", disabled: false }, - ]; - this.usernameTypeOptions = [ - { - name: i18nService.t("plusAddressedEmail"), - value: "subaddress", - desc: i18nService.t("plusAddressedEmailDesc"), - }, - { - name: i18nService.t("catchallEmail"), - value: "catchall", - desc: i18nService.t("catchallEmailDesc"), - }, - { - name: i18nService.t("forwardedEmail"), - value: "forwarded", - desc: i18nService.t("forwardedEmailDesc"), - }, - { name: i18nService.t("randomWord"), value: "word" }, - ]; - this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }]; - this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }]; - - this.forwardOptions = [ - { name: "", value: "", validForSelfHosted: false }, - { name: "addy.io", value: "anonaddy", validForSelfHosted: true }, - { name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false }, - { name: "Fastmail", value: "fastmail", validForSelfHosted: true }, - { name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false }, - { name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true }, - { name: "Forward Email", value: "forwardemail", validForSelfHosted: true }, - ].sort((a, b) => a.name.localeCompare(b.name)); - - this._password.pipe(debounceTime(250)).subscribe((password) => { - ngZone.run(() => { - this.password = password; - }); - this.passwordGenerationService.addHistory(this.password).catch((e) => { - this.logService.error(e); - }); - }); - } - - cascadeOptions(navigationType: GeneratorType = undefined, accountEmail: string) { - this.avoidAmbiguous = !this.passwordOptions.ambiguous; - - if (!this.type) { - if (navigationType) { - this.type = navigationType; - } else { - this.type = this.passwordOptions.type === "username" ? "username" : "password"; - } - } - - this.passwordOptions.type = - this.passwordOptions.type === "passphrase" ? "passphrase" : "password"; - - const overrideType = this.enforcedPasswordPolicyOptions.overridePasswordType ?? ""; - const isDisabled = overrideType.length - ? (value: string, policyValue: string) => value !== policyValue - : (_value: string, _policyValue: string) => false; - for (const option of this._passTypeOptions) { - option.disabled = isDisabled(option.value, overrideType); - } - - if (this.usernameOptions.type == null) { - this.usernameOptions.type = "word"; - } - if ( - this.usernameOptions.subaddressEmail == null || - this.usernameOptions.subaddressEmail === "" - ) { - this.usernameOptions.subaddressEmail = accountEmail; - } - if (this.usernameWebsite == null) { - this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random"; - } else { - this.usernameOptions.website = this.usernameWebsite; - } - } - - async ngOnInit() { - combineLatest([ - this.route.queryParams.pipe(first()), - this.accountService.activeAccount$.pipe(first()), - this.passwordGenerationService.getOptions$(), - this.usernameGenerationService.getOptions$(), - ]) - .pipe( - map(([qParams, account, [passwordOptions, passwordPolicy], usernameOptions]) => ({ - navigationType: qParams.type as GeneratorType, - accountEmail: account.email, - passwordOptions, - passwordPolicy, - usernameOptions, - })), - takeUntil(this.destroy$), - ) - .subscribe((options) => { - this.passwordOptions = options.passwordOptions; - this.enforcedPasswordPolicyOptions = options.passwordPolicy; - this.usernameOptions = options.usernameOptions; - - this.cascadeOptions(options.navigationType, options.accountEmail); - this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength); - - if (this.regenerateWithoutButtonPress()) { - this.regenerate().catch((e) => { - this.logService.error(e); - }); - } - - this.isInitialized$.next(true); - }); - - // once initialization is complete, `ngOnInit` should return. - // - // FIXME(#6944): if a sync is in progress, wait to complete until after - // the sync completes. - await firstValueFrom( - this.isInitialized$.pipe( - skipWhile((initialized) => !initialized), - takeUntil(this.destroy$), - ), - ); - - if (this.usernameWebsite !== null) { - const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" }; - this.subaddressOptions.push(websiteOption); - this.catchallOptions.push(websiteOption); - } - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - this.isInitialized$.complete(); - this._passwordOptionsMinLengthForReader.complete(); - } - - async typeChanged() { - await this.savePasswordOptions(); - } - - async regenerate() { - if (this.type === "password") { - await this.regeneratePassword(); - } else if (this.type === "username") { - await this.regenerateUsername(); - } - } - - async sliderChanged() { - // 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.savePasswordOptions(); - await this.passwordGenerationService.addHistory(this.password); - } - - async onPasswordOptionsMinNumberInput($event: Event) { - // `savePasswordOptions()` replaces the null - this.passwordOptions.number = null; - - await this.savePasswordOptions(); - - // fixes UI desync that occurs when minNumber has a fixed value - // that is reset through normalization - ($event.target as HTMLInputElement).value = `${this.passwordOptions.minNumber}`; - } - - async setPasswordOptionsNumber($event: boolean) { - this.passwordOptions.number = $event; - // `savePasswordOptions()` replaces the null - this.passwordOptions.minNumber = null; - - await this.savePasswordOptions(); - } - - async onPasswordOptionsMinSpecialInput($event: Event) { - // `savePasswordOptions()` replaces the null - this.passwordOptions.special = null; - - await this.savePasswordOptions(); - - // fixes UI desync that occurs when minSpecial has a fixed value - // that is reset through normalization - ($event.target as HTMLInputElement).value = `${this.passwordOptions.minSpecial}`; - } - - async setPasswordOptionsSpecial($event: boolean) { - this.passwordOptions.special = $event; - // `savePasswordOptions()` replaces the null - this.passwordOptions.minSpecial = null; - - await this.savePasswordOptions(); - } - - async sliderInput() { - await this.normalizePasswordOptions(); - } - - async savePasswordOptions() { - // map navigation state into generator type - const restoreType = this.passwordOptions.type; - if (this.type === "username") { - this.passwordOptions.type = this.type; - } - - // save options - await this.normalizePasswordOptions(); - await this.passwordGenerationService.saveOptions(this.passwordOptions); - - // restore the original format - this.passwordOptions.type = restoreType; - } - - async saveUsernameOptions() { - await this.usernameGenerationService.saveOptions(this.usernameOptions); - if (this.usernameOptions.type === "forwarded") { - this.username = "-"; - } - } - - async regeneratePassword() { - this._password.next( - await this.passwordGenerationService.generatePassword(this.passwordOptions), - ); - } - - regenerateUsername() { - return this.generateUsername(); - } - - async generateUsername() { - try { - this.usernameGeneratingPromise = this.usernameGenerationService.generateUsername( - this.usernameOptions, - ); - this.username = await this.usernameGeneratingPromise; - if (this.username === "" || this.username === null) { - this.username = "-"; - } - } catch (e) { - this.logService.error(e); - } - } - - copy() { - const password = this.type === "password"; - const copyOptions = this.win != null ? { window: this.win } : null; - this.platformUtilsService.copyToClipboard( - password ? this.password : this.username, - copyOptions, - ); - this.toastService.showToast({ - variant: "info", - title: null, - message: this.i18nService.t( - "valueCopied", - this.i18nService.t(password ? "password" : "username"), - ), - }); - } - - select() { - this.onSelected.emit(this.type === "password" ? this.password : this.username); - } - - toggleOptions() { - this.showOptions = !this.showOptions; - } - - regenerateWithoutButtonPress() { - return this.type !== "username" || this.usernameOptions.type !== "forwarded"; - } - - private async normalizePasswordOptions() { - // Application level normalize options dependent on class variables - this.passwordOptions.ambiguous = !this.avoidAmbiguous; - - if ( - !this.passwordOptions.uppercase && - !this.passwordOptions.lowercase && - !this.passwordOptions.number && - !this.passwordOptions.special - ) { - this.passwordOptions.lowercase = true; - if (this.win != null) { - const lowercase = this.win.document.querySelector("#lowercase") as HTMLInputElement; - if (lowercase) { - lowercase.checked = true; - } - } - } - - await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions( - this.passwordOptions, - ); - } -} diff --git a/libs/angular/src/tools/generator/components/password-generator-history.component.ts b/libs/angular/src/tools/generator/components/password-generator-history.component.ts deleted file mode 100644 index 2933163fce2f..000000000000 --- a/libs/angular/src/tools/generator/components/password-generator-history.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, OnInit } from "@angular/core"; - -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 { GeneratedPasswordHistory } from "@bitwarden/generator-history"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -@Directive() -export class PasswordGeneratorHistoryComponent implements OnInit { - history: GeneratedPasswordHistory[] = []; - - constructor( - protected passwordGenerationService: PasswordGenerationServiceAbstraction, - protected platformUtilsService: PlatformUtilsService, - protected i18nService: I18nService, - private win: Window, - protected toastService: ToastService, - ) {} - - async ngOnInit() { - this.history = await this.passwordGenerationService.getHistory(); - } - - clear = async () => { - this.history = await this.passwordGenerationService.clear(); - }; - - copy(password: string) { - const copyOptions = this.win != null ? { window: this.win } : null; - this.platformUtilsService.copyToClipboard(password, copyOptions); - this.toastService.showToast({ - variant: "info", - title: null, - message: this.i18nService.t("valueCopied", this.i18nService.t("password")), - }); - } -} diff --git a/libs/angular/src/tools/generator/generator-swap.ts b/libs/angular/src/tools/generator/generator-swap.ts deleted file mode 100644 index 16fafc671169..000000000000 --- a/libs/angular/src/tools/generator/generator-swap.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Type, inject } from "@angular/core"; -import { Route, Routes } from "@angular/router"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -import { componentRouteSwap } from "../../utils/component-route-swap"; - -/** - * Helper function to swap between two components based on the GeneratorToolsModernization 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 the default component, and to the alt component if altOptions is not provided. - * @param altOptions - The alt route options to apply to the alt component. - */ -export function generatorSwap( - defaultComponent: Type, - refreshedComponent: Type, - options: Route, - altOptions?: Route, -): Routes { - return componentRouteSwap( - defaultComponent, - refreshedComponent, - async () => { - const configService = inject(ConfigService); - return configService.getFeatureFlag(FeatureFlag.GeneratorToolsModernization); - }, - options, - altOptions, - ); -} diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index aeee1fa104cc..4f7d4b6b600b 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -16,6 +16,7 @@ import { import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -164,9 +165,10 @@ export class AddEditComponent implements OnInit, OnDestroy { } }); - this.policyService - .getAll$(PolicyType.SendOptions) + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntil(this.destroy$), ) diff --git a/libs/angular/src/utils/extension-refresh-redirect.spec.ts b/libs/angular/src/utils/extension-refresh-redirect.spec.ts deleted file mode 100644 index 3291a4496ff4..000000000000 --- a/libs/angular/src/utils/extension-refresh-redirect.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { TestBed } from "@angular/core/testing"; -import { Navigation, Router, UrlTree } from "@angular/router"; -import { mock, MockProxy } from "jest-mock-extended"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -import { extensionRefreshRedirect } from "./extension-refresh-redirect"; - -describe("extensionRefreshRedirect", () => { - let configService: MockProxy; - let router: MockProxy; - - beforeEach(() => { - configService = mock(); - router = mock(); - - TestBed.configureTestingModule({ - providers: [ - { provide: ConfigService, useValue: configService }, - { provide: Router, useValue: router }, - ], - }); - }); - - it("returns true when ExtensionRefresh flag is disabled", async () => { - configService.getFeatureFlag.mockResolvedValue(false); - - const result = await TestBed.runInInjectionContext(() => - extensionRefreshRedirect("/redirect")(), - ); - - expect(result).toBe(true); - expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.ExtensionRefresh); - expect(router.parseUrl).not.toHaveBeenCalled(); - }); - - it("returns UrlTree when ExtensionRefresh flag is enabled and preserves query params", async () => { - configService.getFeatureFlag.mockResolvedValue(true); - - const urlTree = new UrlTree(); - urlTree.queryParams = { test: "test" }; - - const navigation: Navigation = { - extras: {}, - id: 0, - initialUrl: new UrlTree(), - extractedUrl: urlTree, - trigger: "imperative", - previousNavigation: undefined, - }; - - router.getCurrentNavigation.mockReturnValue(navigation); - - await TestBed.runInInjectionContext(() => extensionRefreshRedirect("/redirect")()); - - expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.ExtensionRefresh); - expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], { - queryParams: urlTree.queryParams, - }); - }); -}); diff --git a/libs/angular/src/utils/extension-refresh-redirect.ts b/libs/angular/src/utils/extension-refresh-redirect.ts deleted file mode 100644 index 2baa3a3ec89e..000000000000 --- a/libs/angular/src/utils/extension-refresh-redirect.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { inject } from "@angular/core"; -import { UrlTree, Router } from "@angular/router"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -/** - * 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) { - const currentNavigation = router.getCurrentNavigation(); - const queryParams = currentNavigation?.extractedUrl?.queryParams || {}; - - // Preserve query params when redirecting as it is likely that the refreshed component - // will be consuming the same query params. - return router.createUrlTree([redirectUrl], { queryParams }); - } else { - return true; - } - }; -} diff --git a/libs/angular/src/utils/extension-refresh-swap.ts b/libs/angular/src/utils/extension-refresh-swap.ts deleted file mode 100644 index 6512be032d28..000000000000 --- a/libs/angular/src/utils/extension-refresh-swap.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Type, inject } from "@angular/core"; -import { Route, Routes } from "@angular/router"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -import { componentRouteSwap } from "./component-route-swap"; - -/** - * 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 the default component, and to the alt component if altOptions is not provided. - * @param altOptions - The alt route options to apply to the alt component. - */ -export function extensionRefreshSwap( - defaultComponent: Type, - refreshedComponent: Type, - options: Route, - altOptions?: Route, -): Routes { - return componentRouteSwap( - defaultComponent, - refreshedComponent, - async () => { - const configService = inject(ConfigService); - return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); - }, - options, - altOptions, - ); -} diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 8d286e0a3f99..7a0c8ae26696 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -7,10 +7,7 @@ import { concatMap, firstValueFrom, map, Observable, Subject, takeUntil } from " import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -235,9 +232,12 @@ export class AddEditComponent implements OnInit, OnDestroy { this.ownershipOptions.push({ name: myEmail, value: null }); } - const orgs = await this.organizationService.getAll(); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); orgs - .filter(isMember) + .filter((org) => org.isMember) .sort(Utils.getSortFunction(this.i18nService, "name")) .forEach((o) => { if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { @@ -313,10 +313,14 @@ export class AddEditComponent implements OnInit, OnDestroy { } // Only Admins can clone a cipher to different owner if (this.cloneMode && this.cipher.organizationId != null) { - const cipherOrg = (await firstValueFrom(this.organizationService.memberOrganizations$)).find( - (o) => o.id === this.cipher.organizationId, + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + const cipherOrg = ( + await firstValueFrom(this.organizationService.memberOrganizations$(activeUserId)) + ).find((o) => o.id === this.cipher.organizationId); + if (cipherOrg != null && !cipherOrg.isAdmin && !cipherOrg.permissions.editAnyCollection) { this.ownershipOptions = [{ name: cipherOrg.name, value: cipherOrg.id }]; } @@ -372,11 +376,11 @@ export class AddEditComponent implements OnInit, OnDestroy { } if (this.cipher.name == null || this.cipher.name === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nameRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nameRequired"), + }); return false; } @@ -385,11 +389,11 @@ export class AddEditComponent implements OnInit, OnDestroy { !this.allowPersonal && this.cipher.organizationId == null ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("personalOwnershipSubmitError"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("personalOwnershipSubmitError"), + }); return false; } @@ -424,11 +428,11 @@ export class AddEditComponent implements OnInit, OnDestroy { this.formPromise = this.saveCipher(cipher); await this.formPromise; this.cipher.id = cipher.id; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem"), + }); this.onSavedCipher.emit(this.cipher); this.messagingService.send(this.editMode && !this.cloneMode ? "editedCipher" : "addedCipher"); return true; @@ -514,11 +518,13 @@ export class AddEditComponent implements OnInit, OnDestroy { try { this.deletePromise = this.deleteCipher(); await this.deletePromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); this.onDeletedCipher.emit(this.cipher); this.messagingService.send( this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher", @@ -538,7 +544,11 @@ export class AddEditComponent implements OnInit, OnDestroy { try { this.restorePromise = this.restoreCipher(); await this.restorePromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); this.onRestoredCipher.emit(this.cipher); this.messagingService.send("restoredCipher"); } catch (e) { @@ -652,7 +662,13 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.collections.length === 1) { (this.collections[0] as any).checked = true; } - const org = await this.organizationService.get(this.cipher.organizationId); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const org = ( + await firstValueFrom(this.organizationService.organizations$(activeUserId)) + ).find((org) => org.id === this.cipher.organizationId); if (org != null) { this.cipher.organizationUseTotp = org.useTotp; } @@ -679,13 +695,17 @@ export class AddEditComponent implements OnInit, OnDestroy { this.checkPasswordPromise = null; if (matches > 0) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("passwordExposed", matches.toString()), - ); + this.toastService.showToast({ + variant: "warning", + title: null, + message: this.i18nService.t("passwordExposed", matches.toString()), + }); } else { - this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordSafe"), + }); } } @@ -779,11 +799,11 @@ export class AddEditComponent implements OnInit, OnDestroy { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), + }); if (typeI18nKey === "password") { void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedPassword, [ diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index 1a4c428aae23..9f1dd31da0cf 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -6,8 +6,8 @@ import { firstValueFrom, map } from "rxjs"; 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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; @@ -16,6 +16,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -26,7 +27,7 @@ import { KeyService } from "@bitwarden/key-management"; export class AttachmentsComponent implements OnInit { @Input() cipherId: string; @Input() viewOnly: boolean; - @Output() onUploadedAttachment = new EventEmitter(); + @Output() onUploadedAttachment = new EventEmitter(); @Output() onDeletedAttachment = new EventEmitter(); @Output() onReuploadedAttachment = new EventEmitter(); @@ -34,7 +35,7 @@ export class AttachmentsComponent implements OnInit { cipherDomain: Cipher; canAccessAttachments: boolean; formPromise: Promise; - deletePromises: { [id: string]: Promise } = {}; + deletePromises: { [id: string]: Promise } = {}; reuploadPromises: { [id: string]: Promise } = {}; emergencyAccessId?: string = null; protected componentName = ""; @@ -64,21 +65,21 @@ export class AttachmentsComponent implements OnInit { const fileEl = document.getElementById("file") as HTMLInputElement; const files = fileEl.files; if (files == null || files.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("selectFile"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); return; } if (files[0].size > 524288000) { // 500 MB - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("maxFileSize"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("maxFileSize"), + }); return; } @@ -91,8 +92,12 @@ export class AttachmentsComponent implements OnInit { this.cipher = await this.cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), ); - this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved")); - this.onUploadedAttachment.emit(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("attachmentSaved"), + }); + this.onUploadedAttachment.emit(this.cipher); } catch (e) { this.logService.error(e); } @@ -121,8 +126,21 @@ export class AttachmentsComponent implements OnInit { try { this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); - await this.deletePromises[attachment.id]; - this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedAttachment")); + const updatedCipher = await this.deletePromises[attachment.id]; + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const cipher = new Cipher(updatedCipher); + this.cipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedAttachment"), + }); const i = this.cipher.attachments.indexOf(attachment); if (i > -1) { this.cipher.attachments.splice(i, 1); @@ -132,7 +150,7 @@ export class AttachmentsComponent implements OnInit { } this.deletePromises[attachment.id] = null; - this.onDeletedAttachment.emit(); + this.onDeletedAttachment.emit(this.cipher); } async download(attachment: AttachmentView) { @@ -142,11 +160,11 @@ export class AttachmentsComponent implements OnInit { } if (!this.canAccessAttachments) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("premiumRequired"), - this.i18nService.t("premiumRequiredDesc"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("premiumRequired"), + message: this.i18nService.t("premiumRequiredDesc"), + }); return; } @@ -171,7 +189,11 @@ export class AttachmentsComponent implements OnInit { a.downloading = true; const response = await fetch(new Request(url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -195,7 +217,11 @@ export class AttachmentsComponent implements OnInit { // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; @@ -243,7 +269,11 @@ export class AttachmentsComponent implements OnInit { a.downloading = true; const response = await fetch(new Request(attachment.url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -281,16 +311,20 @@ export class AttachmentsComponent implements OnInit { } } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("attachmentSaved"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("attachmentSaved"), + }); this.onReuploadedAttachment.emit(); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; diff --git a/libs/angular/src/vault/components/folder-add-edit.component.ts b/libs/angular/src/vault/components/folder-add-edit.component.ts index 205733ba48d1..28ed0dc2aedd 100644 --- a/libs/angular/src/vault/components/folder-add-edit.component.ts +++ b/libs/angular/src/vault/components/folder-add-edit.component.ts @@ -11,7 +11,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @Directive() @@ -43,6 +43,7 @@ export class FolderAddEditComponent implements OnInit { protected logService: LogService, protected dialogService: DialogService, protected formBuilder: FormBuilder, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -52,11 +53,11 @@ export class FolderAddEditComponent implements OnInit { async submit(): Promise { this.folder.name = this.formGroup.controls.name.value; if (this.folder.name == null || this.folder.name === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nameRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nameRequired"), + }); return false; } @@ -66,11 +67,11 @@ export class FolderAddEditComponent implements OnInit { const folder = await this.folderService.encrypt(this.folder, userKey); this.formPromise = this.folderApiService.save(folder, activeUserId); await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), + }); this.onSavedFolder.emit(this.folder); return true; } catch (e) { @@ -95,7 +96,11 @@ export class FolderAddEditComponent implements OnInit { const activeUserId = await firstValueFrom(this.activeUserId$); this.deletePromise = this.folderApiService.delete(this.folder.id, activeUserId); await this.deletePromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedFolder")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedFolder"), + }); this.onDeletedFolder.emit(this.folder); } catch (e) { this.logService.error(e); diff --git a/libs/angular/src/vault/components/icon.component.html b/libs/angular/src/vault/components/icon.component.html index f16545617c99..caca9ded04f5 100644 --- a/libs/angular/src/vault/components/icon.component.html +++ b/libs/angular/src/vault/components/icon.component.html @@ -4,7 +4,7 @@ [src]="data.image" [appFallbackSrc]="data.fallbackImage" *ngIf="data.imageEnabled && data.image" - class="tw-h-6 tw-w-6 tw-rounded-md" + class="tw-size-6 tw-rounded-md" alt="" decoding="async" loading="lazy" diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 942a34c58bbd..0b385688d0b8 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; +import { ToastService } from "@bitwarden/components"; @Directive() export class PasswordHistoryComponent implements OnInit { @@ -20,6 +21,7 @@ export class PasswordHistoryComponent implements OnInit { protected i18nService: I18nService, protected accountService: AccountService, private win: Window, + private toastService: ToastService, ) {} async ngOnInit() { @@ -29,11 +31,11 @@ export class PasswordHistoryComponent implements OnInit { copy(password: string) { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); - 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")), + }); } protected async init() { diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 4ef00e900632..f093aeb1330d 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -1,7 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { BehaviorSubject, firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -40,7 +41,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy { constructor( protected searchService: SearchService, protected cipherService: CipherService, - ) {} + ) { + this.cipherService.cipherViews$.pipe(takeUntilDestroyed()).subscribe((ciphers) => { + void this.doSearch(ciphers); + this.loaded = true; + }); + } ngOnInit(): void { this._searchText$ @@ -117,7 +123,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy { protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; protected async doSearch(indexedCiphers?: CipherView[]) { - indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted()); + indexedCiphers = indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$)); const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$); if (failedCiphers != null && failedCiphers.length > 0) { diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 2b60a175348c..4408eeab93d6 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -11,7 +11,7 @@ import { OnInit, Output, } from "@angular/core"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { filter, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -20,9 +20,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; @@ -40,7 +40,7 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -80,6 +80,7 @@ export class ViewComponent implements OnDestroy, OnInit { private passwordReprompted = false; private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + private destroyed$ = new Subject(); get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); @@ -114,6 +115,7 @@ export class ViewComponent implements OnDestroy, OnInit { protected datePipe: DatePipe, protected accountService: AccountService, private billingAccountProfileStateService: BillingAccountProfileStateService, + protected toastService: ToastService, private cipherAuthorizationService: CipherAuthorizationService, ) {} @@ -142,11 +144,18 @@ export class ViewComponent implements OnDestroy, OnInit { async load() { this.cleanUp(); - const cipher = await this.cipherService.get(this.cipherId); const activeUserId = await firstValueFrom(this.activeUserId$); - this.cipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + // Grab individual cipher from `cipherViews$` for the most up-to-date information + this.cipherService.cipherViews$ + .pipe( + map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)), + filter((cipher) => !!cipher), + takeUntil(this.destroyed$), + ) + .subscribe((cipher) => { + this.cipher = cipher; + }); + this.canAccessPremium = await firstValueFrom( this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); @@ -242,11 +251,13 @@ export class ViewComponent implements OnDestroy, OnInit { try { await this.deleteCipher(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); this.onDeletedCipher.emit(this.cipher); } catch (e) { this.logService.error(e); @@ -262,7 +273,11 @@ export class ViewComponent implements OnDestroy, OnInit { try { await this.restoreCipher(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); this.onRestoredCipher.emit(this.cipher); } catch (e) { this.logService.error(e); @@ -345,13 +360,17 @@ export class ViewComponent implements OnDestroy, OnInit { const matches = await this.checkPasswordPromise; if (matches > 0) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("passwordExposed", matches.toString()), - ); + this.toastService.showToast({ + variant: "warning", + title: null, + message: this.i18nService.t("passwordExposed", matches.toString()), + }); } else { - this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordSafe"), + }); } } @@ -381,11 +400,11 @@ export class ViewComponent implements OnDestroy, OnInit { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), + }); if (typeI18nKey === "password") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -418,11 +437,11 @@ export class ViewComponent implements OnDestroy, OnInit { } if (this.cipher.organizationId == null && !this.canAccessPremium) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("premiumRequired"), - this.i18nService.t("premiumRequiredDesc"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("premiumRequired"), + message: this.i18nService.t("premiumRequiredDesc"), + }); return; } @@ -446,7 +465,11 @@ export class ViewComponent implements OnDestroy, OnInit { a.downloading = true; const response = await fetch(new Request(url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -465,7 +488,11 @@ export class ViewComponent implements OnDestroy, OnInit { // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; @@ -497,6 +524,7 @@ export class ViewComponent implements OnDestroy, OnInit { this.showCardNumber = false; this.showCardCode = false; this.passwordReprompted = false; + this.destroyed$.next(); if (this.totpInterval) { clearInterval(this.totpInterval); } diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts index ba19cf808ee1..938d5b325276 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts @@ -2,16 +2,13 @@ import { TestBed } from "@angular/core/testing"; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router"; import { BehaviorSubject } from "rxjs"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; 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 { NewDeviceVerificationNoticeService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; import { NewDeviceVerificationNoticeGuard } from "./new-device-verification-notice.guard"; @@ -36,19 +33,23 @@ describe("NewDeviceVerificationNoticeGuard", () => { return Promise.resolve(false); }); - const isSelfHost = jest.fn().mockResolvedValue(false); + const isSelfHost = jest.fn().mockReturnValue(false); const getProfileTwoFactorEnabled = jest.fn().mockResolvedValue(false); - const policyAppliesToActiveUser$ = jest.fn().mockReturnValue(new BehaviorSubject(false)); const noticeState$ = jest.fn().mockReturnValue(new BehaviorSubject(null)); const getProfileCreationDate = jest.fn().mockResolvedValue(eightDaysAgo); + const hasMasterPasswordAndMasterKeyHash = jest.fn().mockResolvedValue(true); + const getUserSSOBound = jest.fn().mockResolvedValue(false); + const getUserSSOBoundAdminOwner = jest.fn().mockResolvedValue(false); beforeEach(() => { getFeatureFlag.mockClear(); isSelfHost.mockClear(); getProfileCreationDate.mockClear(); getProfileTwoFactorEnabled.mockClear(); - policyAppliesToActiveUser$.mockClear(); createUrlTree.mockClear(); + hasMasterPasswordAndMasterKeyHash.mockClear(); + getUserSSOBound.mockClear(); + getUserSSOBoundAdminOwner.mockClear(); TestBed.configureTestingModule({ providers: [ @@ -57,10 +58,15 @@ describe("NewDeviceVerificationNoticeGuard", () => { { provide: NewDeviceVerificationNoticeService, useValue: { noticeState$ } }, { provide: AccountService, useValue: { activeAccount$ } }, { provide: PlatformUtilsService, useValue: { isSelfHost } }, - { provide: PolicyService, useValue: { policyAppliesToActiveUser$ } }, + { provide: UserVerificationService, useValue: { hasMasterPasswordAndMasterKeyHash } }, { provide: VaultProfileService, - useValue: { getProfileCreationDate, getProfileTwoFactorEnabled }, + useValue: { + getProfileCreationDate, + getProfileTwoFactorEnabled, + getUserSSOBound, + getUserSSOBoundAdminOwner, + }, }, ], }); @@ -92,7 +98,7 @@ describe("NewDeviceVerificationNoticeGuard", () => { expect(isSelfHost).not.toHaveBeenCalled(); expect(getProfileTwoFactorEnabled).not.toHaveBeenCalled(); expect(getProfileCreationDate).not.toHaveBeenCalled(); - expect(policyAppliesToActiveUser$).not.toHaveBeenCalled(); + expect(hasMasterPasswordAndMasterKeyHash).not.toHaveBeenCalled(); }); }); @@ -123,13 +129,6 @@ describe("NewDeviceVerificationNoticeGuard", () => { expect(await newDeviceGuard()).toBe(true); }); - it("returns `true` SSO is required", async () => { - policyAppliesToActiveUser$.mockReturnValueOnce(new BehaviorSubject(true)); - - expect(await newDeviceGuard()).toBe(true); - expect(policyAppliesToActiveUser$).toHaveBeenCalledWith(PolicyType.RequireSso); - }); - it("returns `true` when the profile was created less than a week ago", async () => { const sixDaysAgo = new Date(); sixDaysAgo.setDate(sixDaysAgo.getDate() - 6); @@ -139,6 +138,63 @@ describe("NewDeviceVerificationNoticeGuard", () => { expect(await newDeviceGuard()).toBe(true); }); + it("returns `true` when the profile service throws an error", async () => { + getProfileCreationDate.mockRejectedValueOnce(new Error("test")); + + expect(await newDeviceGuard()).toBe(true); + }); + + describe("SSO bound", () => { + beforeEach(() => { + getFeatureFlag.mockImplementation((key) => { + if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) { + return Promise.resolve(true); + } + + return Promise.resolve(false); + }); + }); + + afterAll(() => { + getFeatureFlag.mockReturnValue(false); + }); + + it('returns "true" when the user is SSO bound and not an admin or owner', async () => { + getUserSSOBound.mockResolvedValueOnce(true); + getUserSSOBoundAdminOwner.mockResolvedValueOnce(false); + + expect(await newDeviceGuard()).toBe(true); + }); + + it('returns "true" when the user is an admin or owner of an SSO bound organization and has not logged in with their master password', async () => { + getUserSSOBound.mockResolvedValueOnce(true); + getUserSSOBoundAdminOwner.mockResolvedValueOnce(true); + hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(false); + + expect(await newDeviceGuard()).toBe(true); + }); + + it("shows notice when the user is an admin or owner of an SSO bound organization and logged in with their master password", async () => { + getUserSSOBound.mockResolvedValueOnce(true); + getUserSSOBoundAdminOwner.mockResolvedValueOnce(true); + hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true); + + await newDeviceGuard(); + + expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]); + }); + + it("shows notice when the user that is not in an SSO bound organization", async () => { + getUserSSOBound.mockResolvedValueOnce(false); + getUserSSOBoundAdminOwner.mockResolvedValueOnce(false); + hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true); + + await newDeviceGuard(); + + expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]); + }); + }); + describe("temp flag", () => { beforeEach(() => { getFeatureFlag.mockImplementation((key) => { diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts index 8b406877a127..8d4a7742bc5d 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts @@ -1,17 +1,14 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router"; -import { Observable, firstValueFrom } from "rxjs"; +import { firstValueFrom, Observable } from "rxjs"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; 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 { NewDeviceVerificationNoticeService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; export const NewDeviceVerificationNoticeGuard: CanActivateFn = async ( @@ -22,8 +19,8 @@ export const NewDeviceVerificationNoticeGuard: CanActivateFn = async ( const newDeviceVerificationNoticeService = inject(NewDeviceVerificationNoticeService); const accountService = inject(AccountService); const platformUtilsService = inject(PlatformUtilsService); - const policyService = inject(PolicyService); const vaultProfileService = inject(VaultProfileService); + const userVerificationService = inject(UserVerificationService); if (route.queryParams["fromNewDeviceVerification"]) { return true; @@ -47,17 +44,28 @@ export const NewDeviceVerificationNoticeGuard: CanActivateFn = async ( return router.createUrlTree(["/login"]); } - const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id); - const isSelfHosted = await platformUtilsService.isSelfHost(); - const requiresSSO = await isSSORequired(policyService); - const isProfileLessThanWeekOld = await profileIsLessThanWeekOld( - vaultProfileService, - currentAcct.id, - ); - - // When any of the following are true, the device verification notice is - // not applicable for the user. - if (has2FAEnabled || isSelfHosted || requiresSSO || isProfileLessThanWeekOld) { + try { + const isSelfHosted = platformUtilsService.isSelfHost(); + const userIsSSOUser = await ssoAppliesToUser( + userVerificationService, + vaultProfileService, + currentAcct.id, + ); + const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id); + const isProfileLessThanWeekOld = await profileIsLessThanWeekOld( + vaultProfileService, + currentAcct.id, + ); + + // When any of the following are true, the device verification notice is + // not applicable for the user. When the user has *not* logged in with their + // master password, assume they logged in with SSO. + if (has2FAEnabled || isSelfHosted || userIsSSOUser || isProfileLessThanWeekOld) { + return true; + } + } catch { + // Skip showing the notice if there was a problem determining applicability + // The most likely problem to occur is the user not having a network connection return true; } @@ -101,9 +109,39 @@ async function profileIsLessThanWeekOld( return !isMoreThan7DaysAgo(creationDate); } -/** Returns true when the user is required to login via SSO */ -async function isSSORequired(policyService: PolicyService) { - return firstValueFrom(policyService.policyAppliesToActiveUser$(PolicyType.RequireSso)); +/** + * Returns true when either: + * - The user is SSO bound to an organization and is not an Admin or Owner + * - The user is an Admin or Owner of an organization with SSO bound and has not logged in with their master password + * + * NOTE: There are edge cases where this does not satisfy the original requirement of showing the notice to + * users who are subject to the SSO required policy. When Owners and Admins log in with their MP they will see the notice + * when they log in with SSO they will not. This is a concession made because the original logic references policies would not work for TDE users. + * When this guard is run for those users a sync hasn't occurred and thus the policies are not available. + */ +async function ssoAppliesToUser( + userVerificationService: UserVerificationService, + vaultProfileService: VaultProfileService, + userId: string, +) { + const userSSOBound = await vaultProfileService.getUserSSOBound(userId); + const userSSOBoundAdminOwner = await vaultProfileService.getUserSSOBoundAdminOwner(userId); + const userLoggedInWithMP = await userLoggedInWithMasterPassword(userVerificationService, userId); + + const nonOwnerAdminSsoUser = userSSOBound && !userSSOBoundAdminOwner; + const ssoAdminOwnerLoggedInWithMP = userSSOBoundAdminOwner && !userLoggedInWithMP; + + return nonOwnerAdminSsoUser || ssoAdminOwnerLoggedInWithMP; +} + +/** + * Returns true when the user logged in with their master password. + */ +async function userLoggedInWithMasterPassword( + userVerificationService: UserVerificationService, + userId: string, +) { + return userVerificationService.hasMasterPasswordAndMasterKeyHash(userId); } /** Returns the true when the date given is older than 7 days */ diff --git a/libs/angular/src/vault/services/vault-profile.service.spec.ts b/libs/angular/src/vault/services/vault-profile.service.spec.ts index 7761503253a1..ade34da39a6e 100644 --- a/libs/angular/src/vault/services/vault-profile.service.spec.ts +++ b/libs/angular/src/vault/services/vault-profile.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from "@angular/core/testing"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { VaultProfileService } from "./vault-profile.service"; @@ -13,6 +14,12 @@ describe("VaultProfileService", () => { creationDate: hardcodedDateString, twoFactorEnabled: true, id: "new-user-id", + organizations: [ + { + ssoBound: true, + type: OrganizationUserType.Admin, + }, + ], }); beforeEach(() => { @@ -91,4 +98,64 @@ describe("VaultProfileService", () => { expect(getProfile).not.toHaveBeenCalled(); }); }); + + describe("getUserSSOBound", () => { + it("calls `getProfile` when stored ssoBound property is not stored", async () => { + expect(service["userIsSsoBound"]).toBeNull(); + + const userIsSsoBound = await service.getUserSSOBound(userId); + + expect(userIsSsoBound).toBe(true); + expect(getProfile).toHaveBeenCalled(); + }); + + it("calls `getProfile` when stored profile id does not match", async () => { + service["userIsSsoBound"] = false; + service["userId"] = "old-user-id"; + + const userIsSsoBound = await service.getUserSSOBound(userId); + + expect(userIsSsoBound).toBe(true); + expect(getProfile).toHaveBeenCalled(); + }); + + it("does not call `getProfile` when ssoBound property is already stored", async () => { + service["userIsSsoBound"] = false; + + const userIsSsoBound = await service.getUserSSOBound(userId); + + expect(userIsSsoBound).toBe(false); + expect(getProfile).not.toHaveBeenCalled(); + }); + }); + + describe("getUserSSOBoundAdminOwner", () => { + it("calls `getProfile` when stored userIsSsoBoundAdminOwner property is not stored", async () => { + expect(service["userIsSsoBoundAdminOwner"]).toBeNull(); + + const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId); + + expect(userIsSsoBoundAdminOwner).toBe(true); + expect(getProfile).toHaveBeenCalled(); + }); + + it("calls `getProfile` when stored profile id does not match", async () => { + service["userIsSsoBoundAdminOwner"] = false; + service["userId"] = "old-user-id"; + + const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId); + + expect(userIsSsoBoundAdminOwner).toBe(true); + expect(getProfile).toHaveBeenCalled(); + }); + + it("does not call `getProfile` when userIsSsoBoundAdminOwner property is already stored", async () => { + service["userIsSsoBoundAdminOwner"] = false; + + const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId); + + expect(userIsSsoBoundAdminOwner).toBe(false); + expect(getProfile).not.toHaveBeenCalled(); + }); + }); }); diff --git a/libs/angular/src/vault/services/vault-profile.service.ts b/libs/angular/src/vault/services/vault-profile.service.ts index b368a9737815..21f4ecc2285e 100644 --- a/libs/angular/src/vault/services/vault-profile.service.ts +++ b/libs/angular/src/vault/services/vault-profile.service.ts @@ -1,6 +1,7 @@ import { Injectable, inject } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { ProfileResponse } from "@bitwarden/common/models/response/profile.response"; @Injectable({ @@ -24,6 +25,12 @@ export class VaultProfileService { /** True when 2FA is enabled on the profile. */ private profile2FAEnabled: boolean | null = null; + /** True when ssoBound is true for any of the users organizations */ + private userIsSsoBound: boolean | null = null; + + /** True when the user is an admin or owner of the ssoBound organization */ + private userIsSsoBoundAdminOwner: boolean | null = null; + /** * Returns the creation date of the profile. * Note: `Date`s are mutable in JS, creating a new @@ -52,12 +59,43 @@ export class VaultProfileService { return profile.twoFactorEnabled; } + /** + * Returns whether the user logs in with SSO for any organization. + */ + async getUserSSOBound(userId: string): Promise { + if (this.userIsSsoBound !== null && userId === this.userId) { + return Promise.resolve(this.userIsSsoBound); + } + + await this.fetchAndCacheProfile(); + + return !!this.userIsSsoBound; + } + + /** + * Returns true when the user is an Admin or Owner of an organization with `ssoBound` true. + */ + async getUserSSOBoundAdminOwner(userId: string): Promise { + if (this.userIsSsoBoundAdminOwner !== null && userId === this.userId) { + return Promise.resolve(this.userIsSsoBoundAdminOwner); + } + + await this.fetchAndCacheProfile(); + + return !!this.userIsSsoBoundAdminOwner; + } + private async fetchAndCacheProfile(): Promise { const profile = await this.apiService.getProfile(); this.userId = profile.id; this.profileCreatedDate = profile.creationDate; this.profile2FAEnabled = profile.twoFactorEnabled; + const ssoBoundOrg = profile.organizations.find((org) => org.ssoBound); + this.userIsSsoBound = !!ssoBoundOrg; + this.userIsSsoBoundAdminOwner = + ssoBoundOrg?.type === OrganizationUserType.Admin || + ssoBoundOrg?.type === OrganizationUserType.Owner; return profile; } diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index 260780e1964c..aab06a69add7 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -4,28 +4,23 @@ import { Injectable } from "@angular/core"; import { firstValueFrom, from, map, mergeMap, Observable, switchMap } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; +import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state"; import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../abstractions/deprecated-vault-filter.service"; import { DynamicTreeNode } from "../models/dynamic-tree-node.model"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { COLLAPSED_GROUPINGS } from "./../../../../../common/src/vault/services/key-state/collapsed-groupings.state"; - const NestingDelimiter = "/"; @Injectable() @@ -56,9 +51,12 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } async buildOrganizations(): Promise { - let organizations = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + let organizations = await firstValueFrom(this.organizationService.organizations$(userId)); if (organizations != null) { - organizations = organizations.filter(isMember).sort((a, b) => a.name.localeCompare(b.name)); + organizations = organizations + .filter((o) => o.isMember) + .sort((a, b) => a.name.localeCompare(b.name)); } return organizations; diff --git a/libs/angular/tsconfig.json b/libs/angular/tsconfig.json index 6c510f814926..c603e5cf1700 100644 --- a/libs/angular/tsconfig.json +++ b/libs/angular/tsconfig.json @@ -13,9 +13,9 @@ "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], - "@bitwarden/importer/core": ["../importer/src"], "@bitwarden/key-management": ["../key-management/src"], "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"], "@bitwarden/vault": ["../vault/src"] } diff --git a/libs/auth/src/angular/icons/device-verification.icon.ts b/libs/auth/src/angular/icons/device-verification.icon.ts new file mode 100644 index 000000000000..b1be4efdfb34 --- /dev/null +++ b/libs/auth/src/angular/icons/device-verification.icon.ts @@ -0,0 +1,18 @@ +import { svgIcon } from "@bitwarden/components"; + +export const DeviceVerificationIcon = svgIcon` + + + + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 0e86ee7fc8ea..0ec92d54547f 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -12,3 +12,4 @@ export * from "./registration-lock-alt.icon"; export * from "./registration-expired-link.icon"; export * from "./sso-key.icon"; export * from "./two-factor-timeout.icon"; +export * from "./device-verification.icon"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index 66111f3e5af8..67ab68852b27 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -71,3 +71,6 @@ export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.com // login approval export * from "./login-approval/login-approval.component"; export * from "./login-approval/default-login-approval-component.service"; + +// device verification +export * from "./new-device-verification/new-device-verification.component"; diff --git a/libs/auth/src/angular/login-approval/login-approval.component.html b/libs/auth/src/angular/login-approval/login-approval.component.html index c0cb9b9caf4d..2115bdbff11a 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.html +++ b/libs/auth/src/angular/login-approval/login-approval.component.html @@ -1,5 +1,5 @@ - {{ "areYouTryingtoLogin" | i18n }} + {{ "areYouTryingToAccessYourAccount" | i18n }}
@@ -8,7 +8,7 @@ -

{{ "logInAttemptBy" | i18n: email }}

+

{{ "accessAttemptBy" | i18n: email }}

{{ "fingerprintPhraseHeader" | i18n }}

{{ fingerprintPhrase }}

@@ -35,7 +35,7 @@

{{ "logInAttemptBy" | i18n: email }}

[bitAction]="approveLogin" [disabled]="loading" > - {{ "confirmLogIn" | i18n }} + {{ "confirmAccess" | i18n }}

[bitAction]="denyLogin" [disabled]="loading" > - {{ "denyLogIn" | i18n }} + {{ "denyAccess" | i18n }} diff --git a/libs/auth/src/angular/login-approval/login-approval.component.ts b/libs/auth/src/angular/login-approval/login-approval.component.ts index 3b44f545abba..54d90306e5cf 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.ts @@ -85,7 +85,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { } const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey); - this.email = await await firstValueFrom( + this.email = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), ); this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index a3f5e062e4f2..4c93c79d6fe3 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -202,7 +202,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { }); const autoEnrollStatus$ = defer(() => - this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(), + this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId), ).pipe( switchMap((organizationIdentifier) => { if (organizationIdentifier == undefined) { diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html index a1d0f200c151..ba26ba77cb06 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html @@ -1,6 +1,20 @@
-

{{ "makeSureYourAccountIsUnlockedAndTheFingerprintEtc" | i18n }}

+

+ {{ "notificationSentDevicePart1" | i18n }} + {{ "notificationSentDeviceAnchor" | i18n }}. {{ "notificationSentDevicePart2" | i18n }} +

+

+ {{ "notificationSentDeviceComplete" | i18n }} +

{{ "fingerprintPhraseHeader" | i18n }}
{{ fingerprintPhrase }} diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index b9a5ee4fe735..00e2d621c47d 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -29,6 +29,7 @@ import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.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"; @@ -71,6 +72,8 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { protected showResendNotification = false; protected Flow = Flow; protected flow = Flow.StandardAuthRequest; + protected webVaultUrl: string; + protected deviceManagementUrl: string; constructor( private accountService: AccountService, @@ -81,6 +84,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private authService: AuthService, private cryptoFunctionService: CryptoFunctionService, private deviceTrustService: DeviceTrustServiceAbstraction, + private environmentService: EnvironmentService, private i18nService: I18nService, private logService: LogService, private loginEmailService: LoginEmailServiceAbstraction, @@ -109,6 +113,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { this.logService.error("Failed to use approved auth request: " + e.message); }); }); + + // Get the web vault URL from the environment service + this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { + this.webVaultUrl = env.getWebVaultUrl(); + this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`; + }); } async ngOnInit(): Promise { diff --git a/libs/auth/src/angular/login/login-secondary-content.component.ts b/libs/auth/src/angular/login/login-secondary-content.component.ts index dbc9535e67a2..b608542b3759 100644 --- a/libs/auth/src/angular/login/login-secondary-content.component.ts +++ b/libs/auth/src/angular/login/login-secondary-content.component.ts @@ -3,7 +3,6 @@ import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service"; import { LinkModule } from "@bitwarden/components"; @@ -13,16 +12,12 @@ import { LinkModule } from "@bitwarden/components"; template: ` `, }) export class LoginSecondaryContentComponent { - registerRouteService = inject(RegisterRouteService); serverSettingsService = inject(DefaultServerSettingsService); - // TODO: remove when email verification flag is removed - protected registerRoute$ = this.registerRouteService.registerRoute$(); - protected isUserRegistrationDisabled$ = this.serverSettingsService.isUserRegistrationDisabled$; } diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index f9aaa5d1e059..66fe25035081 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -275,6 +275,12 @@ export class LoginComponent implements OnInit, OnDestroy { return; } + // Redirect to device verification if this is an unknown device + if (authResult.requiresDeviceVerification) { + await this.router.navigate(["device-verification"]); + return; + } + await this.loginSuccessHandlerService.run(authResult.userId); if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.html b/libs/auth/src/angular/new-device-verification/new-device-verification.component.html new file mode 100644 index 000000000000..2f807d329935 --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.html @@ -0,0 +1,36 @@ +
+ + {{ "verificationCode" | i18n }} + + + + + +
+ +
+
diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts new file mode 100644 index 000000000000..6e0f9eec05ee --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -0,0 +1,163 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { + AsyncActionsModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ToastService, +} from "@bitwarden/components"; + +import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service"; +import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service"; +import { PasswordLoginStrategy } from "../../common/login-strategies/password-login.strategy"; + +/** + * Component for verifying a new device via a one-time password (OTP). + */ +@Component({ + standalone: true, + selector: "app-new-device-verification", + templateUrl: "./new-device-verification.component.html", + imports: [ + CommonModule, + ReactiveFormsModule, + AsyncActionsModule, + JslibModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ], +}) +export class NewDeviceVerificationComponent implements OnInit, OnDestroy { + formGroup = this.formBuilder.group({ + code: [ + "", + { + validators: [Validators.required], + updateOn: "change", + }, + ], + }); + + protected disableRequestOTP = false; + private destroy$ = new Subject(); + protected authenticationSessionTimeoutRoute = "/authentication-timeout"; + + constructor( + private router: Router, + private formBuilder: FormBuilder, + private passwordLoginStrategy: PasswordLoginStrategy, + private apiService: ApiService, + private loginStrategyService: LoginStrategyServiceAbstraction, + private logService: LogService, + private toastService: ToastService, + private i18nService: I18nService, + private syncService: SyncService, + private loginEmailService: LoginEmailServiceAbstraction, + ) {} + + async ngOnInit() { + // Redirect to timeout route if session expires + this.loginStrategyService.authenticationSessionTimeout$ + .pipe(takeUntil(this.destroy$)) + .subscribe((expired) => { + if (!expired) { + return; + } + + try { + void this.router.navigate([this.authenticationSessionTimeoutRoute]); + } catch (err) { + this.logService.error( + `Failed to navigate to ${this.authenticationSessionTimeoutRoute} route`, + err, + ); + } + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + /** + * Resends the OTP for device verification. + */ + async resendOTP() { + this.disableRequestOTP = true; + try { + const email = await this.loginStrategyService.getEmail(); + const masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash(); + + if (!email || !masterPasswordHash) { + throw new Error("Missing email or master password hash"); + } + + await this.apiService.send( + "POST", + "/accounts/resend-new-device-otp", + { + email: email, + masterPasswordHash: masterPasswordHash, + }, + false, + false, + ); + } catch (e) { + this.logService.error(e); + } finally { + this.disableRequestOTP = false; + } + } + + /** + * Submits the OTP for device verification. + */ + submit = async (): Promise => { + const codeControl = this.formGroup.get("code"); + if (!codeControl || !codeControl.value) { + return; + } + + try { + const authResult = await this.loginStrategyService.logInNewDeviceVerification( + codeControl.value, + ); + + if (authResult.requiresTwoFactor) { + await this.router.navigate(["/2fa"]); + return; + } + + if (authResult.forcePasswordReset) { + await this.router.navigate(["/update-temp-password"]); + return; + } + + this.loginEmailService.clearValues(); + + await this.syncService.fullSync(true); + + // If verification succeeds, navigate to vault + await this.router.navigate(["/vault"]); + } catch (e) { + this.logService.error(e); + const errorMessage = + (e as any)?.response?.error_description ?? this.i18nService.t("errorOccurred"); + codeControl.setErrors({ serverError: { message: errorMessage } }); + } + }; +} diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 31b3f7db92a2..c419e1f427f1 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -16,7 +16,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; -import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "../../../common"; +import { + LoginStrategyServiceAbstraction, + LoginSuccessHandlerService, + PasswordLoginCredentials, +} from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { InputPasswordComponent } from "../../input-password/input-password.component"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -68,6 +72,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { private loginStrategyService: LoginStrategyServiceAbstraction, private logService: LogService, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) {} async ngOnInit() { @@ -189,6 +194,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { message: this.i18nService.t("youHaveBeenLoggedIn"), }); + await this.loginSuccessHandlerService.run(authenticationResult.userId); + await this.router.navigate(["/vault"]); } catch (e) { // If login errors, redirect to login page per product. Don't show error diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts index 7a2b334eb423..726110663fc9 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts @@ -11,8 +11,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts index 84c580662be5..6c9ce8f9267c 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts @@ -12,8 +12,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index 3bcc56ae4cdf..b3f0d7d6a662 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -367,7 +367,7 @@ export class SsoComponent implements OnInit { codeVerifier, redirectUri, orgSsoIdentifier, - email, + email ?? undefined, ); this.formPromise = this.loginStrategyService.logIn(credentials); const authResult = await this.formPromise; @@ -384,7 +384,10 @@ export class SsoComponent implements OnInit { // - TDE login decryption options component // - Browser SSO on extension open // Note: you cannot set this in state before 2FA b/c there won't be an account in state. - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier); + + // Grabbing the active user id right before making the state set to ensure it exists. + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId); // Users enrolled in admin acct recovery can be forced to set a new password after // having the admin set a temp password for them (affects TDE & standard users) @@ -480,16 +483,7 @@ export class SsoComponent implements OnInit { } private async handleChangePasswordRequired(orgIdentifier: string) { - const emailVerification = await this.configService.getFeatureFlag( - FeatureFlag.EmailVerification, - ); - - let route = "set-password"; - if (emailVerification) { - route = "set-password-jit"; - } - - await this.router.navigate([route], { + await this.router.navigate(["set-password-jit"], { queryParams: { identifier: orgIdentifier, }, diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.html b/libs/auth/src/angular/user-verification/user-verification-form-input.component.html index f532a3b23fd2..56bce040d2ff 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.html +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.html @@ -120,7 +120,7 @@
{{ "enterVerificationCodeSentToEmail" | i18n }} -

+

diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index 1088d6de7365..bd725f290249 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -47,7 +47,6 @@ export abstract class LoginStrategyServiceAbstraction { * Auth Request. Otherwise, it will return null. */ getAuthRequestId: () => Promise; - /** * Sends a token request to the server using the provided credentials. */ @@ -74,7 +73,11 @@ export abstract class LoginStrategyServiceAbstraction { */ makePreloginKey: (masterPassword: string, email: string) => Promise; /** - * Emits true if the two factor session has expired. + * Emits true if the authentication session has expired. + */ + authenticationSessionTimeout$: Observable; + /** + * Sends a token request to the server with the provided device verification OTP. */ - twoFactorTimeout$: Observable; + logInNewDeviceVerification: (deviceVerificationOtp: string) => Promise; } diff --git a/libs/auth/src/common/index.ts b/libs/auth/src/common/index.ts index 43efd7c63879..97909bdc4498 100644 --- a/libs/auth/src/common/index.ts +++ b/libs/auth/src/common/index.ts @@ -6,3 +6,4 @@ export * from "./models"; export * from "./types"; export * from "./services"; export * from "./utilities"; +export * from "./login-strategies"; diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index cec4481cd8db..7c56e2a58c8f 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -9,8 +9,8 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; diff --git a/libs/auth/src/common/login-strategies/index.ts b/libs/auth/src/common/login-strategies/index.ts new file mode 100644 index 000000000000..166ef935e08e --- /dev/null +++ b/libs/auth/src/common/login-strategies/index.ts @@ -0,0 +1 @@ +export { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy"; diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 50443bab0ea1..fbd6b79f19d9 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -12,6 +13,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; @@ -19,8 +21,8 @@ import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/mod import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; @@ -76,8 +78,8 @@ const twoFactorToken = "TWO_FACTOR_TOKEN"; const twoFactorRemember = true; export function identityTokenResponseFactory( - masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null, - userDecryptionOptions: IUserDecryptionOptionsServerResponse = null, + masterPasswordPolicyResponse: MasterPasswordPolicyResponse | undefined = undefined, + userDecryptionOptions: IUserDecryptionOptionsServerResponse | undefined = undefined, ) { return new IdentityTokenResponse({ ForcePasswordReset: false, @@ -155,7 +157,7 @@ describe("LoginStrategy", () => { passwordStrengthService, policyService, loginStrategyService, - accountService, + accountService as unknown as AccountService, masterPasswordService, keyService, encryptService, @@ -286,13 +288,16 @@ describe("LoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); - expect(result).toEqual({ - userId: userId, - forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset, - resetMasterPassword: true, - twoFactorProviders: null, - captchaSiteKey: "", - } as AuthResult); + const expected = new AuthResult(); + expected.userId = userId; + expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; + expected.resetMasterPassword = true; + expected.twoFactorProviders = {} as Partial< + Record> + >; + expected.captchaSiteKey = ""; + expected.twoFactorProviders = null; + expect(result).toEqual(expected); }); it("rejects login if CAPTCHA is required", async () => { @@ -377,10 +382,11 @@ describe("LoginStrategy", () => { expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = { 0: null } as Record< - TwoFactorProviderType, - Record + expected.twoFactorProviders = { 0: null } as unknown as Partial< + Record> >; + expected.email = ""; + expected.ssoEmail2FaSessionToken = undefined; expect(result).toEqual(expected); }); @@ -460,14 +466,19 @@ describe("LoginStrategy", () => { it("sends 2FA token provided by user to server (two-step)", async () => { // Simulate a partially completed login cache = new PasswordLoginStrategyData(); - cache.tokenRequest = new PasswordTokenRequest(email, masterPasswordHash, null, null); + cache.tokenRequest = new PasswordTokenRequest( + email, + masterPasswordHash, + "", + new TokenTwoFactorRequest(), + ); passwordLoginStrategy = new PasswordLoginStrategy( cache, passwordStrengthService, policyService, loginStrategyService, - accountService, + accountService as AccountService, masterPasswordService, keyService, encryptService, @@ -489,7 +500,7 @@ describe("LoginStrategy", () => { await passwordLoginStrategy.logInTwoFactor( new TokenTwoFactorRequest(twoFactorProviderType, twoFactorToken, twoFactorRemember), - null, + "", ); expect(apiService.postIdentityToken).toHaveBeenCalledWith( @@ -503,4 +514,54 @@ describe("LoginStrategy", () => { ); }); }); + + describe("Device verification", () => { + it("processes device verification response", async () => { + const captchaToken = "test-captcha-token"; + const deviceVerificationResponse = new IdentityDeviceVerificationResponse({ + error: "invalid_grant", + error_description: "Device verification required.", + email: "test@bitwarden.com", + deviceVerificationRequest: true, + captchaToken: captchaToken, + }); + + apiService.postIdentityToken.mockResolvedValue(deviceVerificationResponse); + + cache = new PasswordLoginStrategyData(); + cache.tokenRequest = new PasswordTokenRequest( + email, + masterPasswordHash, + "", + new TokenTwoFactorRequest(), + ); + + passwordLoginStrategy = new PasswordLoginStrategy( + cache, + passwordStrengthService, + policyService, + loginStrategyService, + accountService as AccountService, + masterPasswordService, + keyService, + encryptService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + twoFactorService, + userDecryptionOptionsService, + billingAccountProfileStateService, + vaultTimeoutSettingsService, + kdfConfigService, + ); + + const result = await passwordLoginStrategy.logIn(credentials); + + expect(result.requiresDeviceVerification).toBe(true); + }); + }); }); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 25f99f478404..b4eef1a02764 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; +import { BehaviorSubject, filter, firstValueFrom, timeout, Observable } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -18,14 +16,15 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request"; import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; @@ -51,14 +50,19 @@ import { import { UserDecryptionOptions } from "../models/domain/user-decryption-options"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse; +type IdentityResponse = + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse; export abstract class LoginStrategyData { tokenRequest: | UserApiTokenRequest | PasswordTokenRequest | SsoTokenRequest - | WebAuthnLoginTokenRequest; + | WebAuthnLoginTokenRequest + | undefined; captchaBypassToken?: string; /** User's entered email obtained pre-login. */ @@ -67,6 +71,8 @@ export abstract class LoginStrategyData { export abstract class LoginStrategy { protected abstract cache: BehaviorSubject; + protected sessionTimeoutSubject = new BehaviorSubject(false); + sessionTimeout$: Observable = this.sessionTimeoutSubject.asObservable(); constructor( protected accountService: AccountService, @@ -100,9 +106,12 @@ export abstract class LoginStrategy { async logInTwoFactor( twoFactor: TokenTwoFactorRequest, - captchaResponse: string = null, + captchaResponse: string | null = null, ): Promise { const data = this.cache.value; + if (!data.tokenRequest) { + throw new Error("Token request is undefined"); + } data.tokenRequest.setTwoFactor(twoFactor); this.cache.next(data); const [authResult] = await this.startLogIn(); @@ -113,6 +122,9 @@ export abstract class LoginStrategy { await this.twoFactorService.clearSelectedProvider(); const tokenRequest = this.cache.value.tokenRequest; + if (!tokenRequest) { + throw new Error("Token request is undefined"); + } const response = await this.apiService.postIdentityToken(tokenRequest); if (response instanceof IdentityTwoFactorResponse) { @@ -121,6 +133,8 @@ export abstract class LoginStrategy { return [await this.processCaptchaResponse(response), response]; } else if (response instanceof IdentityTokenResponse) { return [await this.processTokenResponse(response), response]; + } else if (response instanceof IdentityDeviceVerificationResponse) { + return [await this.processDeviceVerificationResponse(response), response]; } throw new Error("Invalid response object."); @@ -176,8 +190,8 @@ export abstract class LoginStrategy { await this.accountService.addAccount(userId, { name: accountInformation.name, - email: accountInformation.email, - emailVerified: accountInformation.email_verified, + email: accountInformation.email ?? "", + emailVerified: accountInformation.email_verified ?? false, }); await this.accountService.switchAccount(userId); @@ -230,7 +244,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium( - accountInformation.premium, + accountInformation.premium ?? false, false, userId, ); @@ -291,6 +305,9 @@ export abstract class LoginStrategy { try { const userKey = await this.keyService.getUserKeyWithLegacySupport(userId); const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey); + if (!privateKey.encryptedString) { + throw new Error("Failed to create encrypted private key"); + } await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString)); return privateKey.encryptedString; } catch (e) { @@ -316,7 +333,8 @@ export abstract class LoginStrategy { await this.twoFactorService.setProviders(response); this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; - result.email = response.email; + + result.email = response.email ?? ""; return result; } @@ -355,4 +373,22 @@ export abstract class LoginStrategy { ), ); } + + /** + * Handles the response from the server when a device verification is required. + * It sets the requiresDeviceVerification flag to true and caches the captcha token if it came back. + * + * @param {IdentityDeviceVerificationResponse} response - The response from the server indicating that device verification is required. + * @returns {Promise} - A promise that resolves to an AuthResult object + */ + protected async processDeviceVerificationResponse( + response: IdentityDeviceVerificationResponse, + ): Promise { + const result = new AuthResult(); + result.requiresDeviceVerification = true; + + // Extend cached data with captcha bypass token if it came back. + this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); + return result; + } } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 4ee4fcaeb388..1b0613d4da3d 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -13,8 +13,8 @@ import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/resp import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; @@ -276,4 +276,24 @@ describe("PasswordLoginStrategy", () => { ); expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); + + it("handles new device verification login with OTP", async () => { + const deviceVerificationOtp = "123456"; + const tokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValueOnce(tokenResponse); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); + + await passwordLoginStrategy.logIn(credentials); + + const result = await passwordLoginStrategy.logInNewDeviceVerification(deviceVerificationOtp); + + expect(apiService.postIdentityToken).toHaveBeenCalledWith( + expect.objectContaining({ + newDeviceOtp: deviceVerificationOtp, + }), + ); + expect(result.forcePasswordReset).toBe(ForceSetPasswordReason.None); + expect(result.resetMasterPassword).toBe(false); + expect(result.userId).toBe(userId); + }); }); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index c496b7c96742..f0a8d40f9145 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -10,6 +10,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { HashPurpose } from "@bitwarden/common/platform/enums"; @@ -208,9 +209,12 @@ export class PasswordLoginStrategy extends LoginStrategy { } private getMasterPasswordPolicyOptionsFromResponse( - response: IdentityTokenResponse | IdentityTwoFactorResponse, + response: + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse, ): MasterPasswordPolicyOptions { - if (response == null) { + if (response == null || response instanceof IdentityDeviceVerificationResponse) { return null; } return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy); @@ -233,4 +237,13 @@ export class PasswordLoginStrategy extends LoginStrategy { password: this.cache.value, }; } + + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + const data = this.cache.value; + data.tokenRequest.newDeviceOtp = deviceVerificationOtp; + this.cache.next(data); + + const [authResult] = await this.startLogIn(); + return authResult; + } } diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index ec3ec43134fa..96b08e98e372 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -13,9 +13,9 @@ import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/mod import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 2bb41faa0e10..dd3e7f0134d7 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -8,8 +8,8 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Environment, EnvironmentService, diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 9dacce2cf00b..fd8817a8c214 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -11,8 +11,8 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 86b2a1dd3b64..2ea6d4276414 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -3,9 +3,9 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index 4bc0397b43a6..5bc200ae1e89 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -9,9 +9,9 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index d1cedebcf36a..44f6afa5d234 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -4,6 +4,5 @@ export * from "./login-strategies/login-strategy.service"; export * from "./user-decryption-options/user-decryption-options.service"; export * from "./auth-request/auth-request.service"; export * from "./auth-request/auth-request-api.service"; -export * from "./register-route.service"; export * from "./accounts/lock.service"; export * from "./login-success-handler/default-login-success-handler.service"; diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 5fcbefbef2f0..117e5c1f864d 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -17,8 +17,8 @@ import { PreloginResponse } from "@bitwarden/common/auth/models/response/prelogi import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; @@ -321,4 +321,67 @@ describe("LoginStrategyService", () => { `PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1}; possible pre-login downgrade attack detected.`, ); }); + + it("returns an AuthResult on successful new device verification", async () => { + const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD"); + const deviceVerificationOtp = "123456"; + + // Setup initial login and device verification response + apiService.postPrelogin.mockResolvedValue( + new PreloginResponse({ + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + }), + ); + + apiService.postIdentityToken.mockResolvedValueOnce( + new IdentityTwoFactorResponse({ + TwoFactorProviders: ["0"], + TwoFactorProviders2: { 0: null }, + error: "invalid_grant", + error_description: "Two factor required.", + email: undefined, + ssoEmail2faSessionToken: undefined, + }), + ); + + await sut.logIn(credentials); + + // Successful device verification login + apiService.postIdentityToken.mockResolvedValueOnce( + new IdentityTokenResponse({ + ForcePasswordReset: false, + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + Key: "KEY", + PrivateKey: "PRIVATE_KEY", + ResetMasterPassword: false, + access_token: "ACCESS_TOKEN", + expires_in: 3600, + refresh_token: "REFRESH_TOKEN", + scope: "api offline_access", + token_type: "Bearer", + }), + ); + + tokenService.decodeAccessToken.calledWith("ACCESS_TOKEN").mockResolvedValue({ + sub: "USER_ID", + name: "NAME", + email: "EMAIL", + premium: false, + }); + + const result = await sut.logInNewDeviceVerification(deviceVerificationOtp); + + expect(result).toBeInstanceOf(AuthResult); + expect(apiService.postIdentityToken).toHaveBeenCalledWith( + expect.objectContaining({ + newDeviceOtp: deviceVerificationOtp, + }), + ); + }); }); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 57a653b205e7..849b8e5eba1b 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { combineLatestWith, distinctUntilChanged, @@ -15,6 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -23,10 +22,10 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; @@ -35,9 +34,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; import { @@ -51,12 +47,24 @@ import { import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; -import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy"; +import { + AuthRequestLoginStrategy, + AuthRequestLoginStrategyData, +} from "../../login-strategies/auth-request-login.strategy"; import { LoginStrategy } from "../../login-strategies/login.strategy"; -import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy"; -import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy"; -import { UserApiLoginStrategy } from "../../login-strategies/user-api-login.strategy"; -import { WebAuthnLoginStrategy } from "../../login-strategies/webauthn-login.strategy"; +import { + PasswordLoginStrategy, + PasswordLoginStrategyData, +} from "../../login-strategies/password-login.strategy"; +import { SsoLoginStrategy, SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy"; +import { + UserApiLoginStrategy, + UserApiLoginStrategyData, +} from "../../login-strategies/user-api-login.strategy"; +import { + WebAuthnLoginStrategy, + WebAuthnLoginStrategyData, +} from "../../login-strategies/webauthn-login.strategy"; import { UserApiLoginCredentials, PasswordLoginCredentials, @@ -76,14 +84,15 @@ import { const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes export class LoginStrategyService implements LoginStrategyServiceAbstraction { - private sessionTimeoutSubscription: Subscription; + private sessionTimeoutSubscription: Subscription | undefined; private currentAuthnTypeState: GlobalState; private loginStrategyCacheState: GlobalState; private loginStrategyCacheExpirationState: GlobalState; - private authRequestPushNotificationState: GlobalState; - private twoFactorTimeoutSubject = new BehaviorSubject(false); + private authRequestPushNotificationState: GlobalState; + private authenticationTimeoutSubject = new BehaviorSubject(false); - twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable(); + authenticationSessionTimeout$: Observable = + this.authenticationTimeoutSubject.asObservable(); private loginStrategy$: Observable< | UserApiLoginStrategy @@ -132,7 +141,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, async () => { - this.twoFactorTimeoutSubject.next(true); + this.authenticationTimeoutSubject.next(true); try { await this.clearCache(); } catch (e) { @@ -153,7 +162,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getEmail(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("email$" in strategy) { + if (strategy && "email$" in strategy) { return await firstValueFrom(strategy.email$); } return null; @@ -162,7 +171,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getMasterPasswordHash(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("serverMasterKeyHash$" in strategy) { + if (strategy && "serverMasterKeyHash$" in strategy) { return await firstValueFrom(strategy.serverMasterKeyHash$); } return null; @@ -171,7 +180,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getSsoEmail2FaSessionToken(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("ssoEmail2FaSessionToken$" in strategy) { + if (strategy && "ssoEmail2FaSessionToken$" in strategy) { return await firstValueFrom(strategy.ssoEmail2FaSessionToken$); } return null; @@ -180,7 +189,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getAccessCode(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("accessCode$" in strategy) { + if (strategy && "accessCode$" in strategy) { return await firstValueFrom(strategy.accessCode$); } return null; @@ -189,7 +198,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getAuthRequestId(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("authRequestId$" in strategy) { + if (strategy && "authRequestId$" in strategy) { return await firstValueFrom(strategy.authRequestId$); } return null; @@ -204,7 +213,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { | WebAuthnLoginCredentials, ): Promise { await this.clearCache(); - this.twoFactorTimeoutSubject.next(false); + this.authenticationTimeoutSubject.next(false); await this.currentAuthnTypeState.update((_) => credentials.type); @@ -217,16 +226,19 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { // If the popup uses its own instance of this service, this can be removed. const ownedCredentials = { ...credentials }; - const result = await strategy.logIn(ownedCredentials as any); + const result = await strategy?.logIn(ownedCredentials as any); - if (result != null && !result.requiresTwoFactor) { + if (result != null && !result.requiresTwoFactor && !result.requiresDeviceVerification) { await this.clearCache(); } else { - // Cache the strategy data so we can attempt again later with 2fa. Cache supports different contexts - await this.loginStrategyCacheState.update((_) => strategy.exportCache()); + // Cache the strategy data so we can attempt again later with 2fa or device verification + await this.loginStrategyCacheState.update((_) => strategy?.exportCache() ?? null); await this.startSessionTimeout(); } + if (!result) { + throw new Error("No auth result returned"); + } return result; } @@ -260,9 +272,46 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { } } + /** + * Sends a token request to the server with the provided device verification OTP. + * Returns an error if no session data is found or if the current login strategy does not support device verification. + * @param deviceVerificationOtp The OTP to send to the server for device verification. + * @returns The result of the token request. + */ + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + if (!(await this.isSessionValid())) { + throw new Error(this.i18nService.t("sessionTimeout")); + } + + const strategy = await firstValueFrom(this.loginStrategy$); + if (strategy == null) { + throw new Error("No login strategy found."); + } + + if (!("logInNewDeviceVerification" in strategy)) { + throw new Error("Current login strategy does not support device verification."); + } + + try { + const result = await strategy.logInNewDeviceVerification(deviceVerificationOtp); + + // Only clear cache if device verification succeeds + if (result !== null && !result.requiresDeviceVerification) { + await this.clearCache(); + } + return result; + } catch (e) { + // Clear the cache if there is an unhandled client-side error + if (!(e instanceof ErrorResponse)) { + await this.clearCache(); + } + throw e; + } + } + async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - let kdfConfig: KdfConfig = null; + let kdfConfig: KdfConfig | undefined; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { @@ -275,12 +324,15 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { preloginResponse.kdfParallelism, ); } - } catch (e) { + } catch (e: any) { if (e == null || e.statusCode !== 404) { throw e; } } + if (!kdfConfig) { + throw new Error("KDF config is required"); + } kdfConfig.validateKdfConfigForPrelogin(); return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig); @@ -289,7 +341,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private async clearCache(): Promise { await this.currentAuthnTypeState.update((_) => null); await this.loginStrategyCacheState.update((_) => null); - this.twoFactorTimeoutSubject.next(false); + this.authenticationTimeoutSubject.next(false); await this.clearSessionTimeout(); } @@ -360,7 +412,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { switch (strategy) { case AuthenticationType.Password: return new PasswordLoginStrategy( - data?.password, + data?.password ?? new PasswordLoginStrategyData(), this.passwordStrengthService, this.policyService, this, @@ -368,7 +420,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); case AuthenticationType.Sso: return new SsoLoginStrategy( - data?.sso, + data?.sso ?? new SsoLoginStrategyData(), this.keyConnectorService, this.deviceTrustService, this.authRequestService, @@ -377,19 +429,22 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( - data?.userApiKey, + data?.userApiKey ?? new UserApiLoginStrategyData(), this.environmentService, this.keyConnectorService, ...sharedDeps, ); case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( - data?.authRequest, + data?.authRequest ?? new AuthRequestLoginStrategyData(), this.deviceTrustService, ...sharedDeps, ); case AuthenticationType.WebAuthn: - return new WebAuthnLoginStrategy(data?.webAuthn, ...sharedDeps); + return new WebAuthnLoginStrategy( + data?.webAuthn ?? new WebAuthnLoginStrategyData(), + ...sharedDeps, + ); } }), ); diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index 01fc77e4a037..9b86440b364b 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -4,8 +4,8 @@ import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; diff --git a/libs/auth/src/common/services/pin/pin.service.spec.ts b/libs/auth/src/common/services/pin/pin.service.spec.ts index d254be4e875b..1d6443535bca 100644 --- a/libs/auth/src/common/services/pin/pin.service.spec.ts +++ b/libs/auth/src/common/services/pin/pin.service.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; diff --git a/libs/auth/src/common/services/register-route.service.ts b/libs/auth/src/common/services/register-route.service.ts deleted file mode 100644 index 5bc09db699ea..000000000000 --- a/libs/auth/src/common/services/register-route.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Observable, map } from "rxjs"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -// This is a temporary service to determine the correct route to use for registration based on the email verification feature flag. -export class RegisterRouteService { - constructor(private configService: ConfigService) {} - - registerRoute$(): Observable { - return this.configService.getFeatureFlag$(FeatureFlag.EmailVerification).pipe( - map((emailVerificationEnabled) => { - if (emailVerificationEnabled) { - return "/signup"; - } else { - return "/register"; - } - }), - ); - } -} diff --git a/libs/auth/tsconfig.json b/libs/auth/tsconfig.json index 9be942d38de5..8d08522ffcee 100644 --- a/libs/auth/tsconfig.json +++ b/libs/auth/tsconfig.json @@ -4,17 +4,18 @@ "resolveJsonModule": true, "paths": { "@bitwarden/admin-console/common": ["../admin-console/src/common"], - "@bitwarden/auth/common": ["../auth/src/common"], - "@bitwarden/auth/angular": ["../auth/src/angular"], "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/angular": ["../auth/src/angular"], + "@bitwarden/auth/common": ["../auth/src/common"], "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/components": ["../components/src"], - "@bitwarden/key-management": ["../key-management/src"], - "@bitwarden/platform": ["../platform/src"], "@bitwarden/generator-core": ["../tools/generator/core/src"], "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], - "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"] + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"] } }, "include": ["src", "spec"], diff --git a/libs/common/package.json b/libs/common/package.json index 5e0f5ae20c6d..ad2771e2fffd 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -15,6 +15,7 @@ "scripts": { "clean": "rimraf dist", "build": "npm run clean && tsc", - "build:watch": "npm run clean && tsc -watch" + "build:watch": "npm run clean && tsc -watch", + "test": "jest" } } diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index 05e44d5db18c..ba48181faa21 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { mock } from "jest-mock-extended"; -import { ReplaySubject, combineLatest, map } from "rxjs"; +import { ReplaySubject, combineLatest, map, Observable } from "rxjs"; import { Account, AccountInfo, AccountService } from "../src/auth/abstractions/account.service"; import { UserId } from "../src/types/guid"; @@ -35,6 +35,8 @@ export class FakeAccountService implements AccountService { activeAccountSubject = new ReplaySubject(1); // eslint-disable-next-line rxjs/no-exposed-subjects -- test class accountActivitySubject = new ReplaySubject>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + accountVerifyDevicesSubject = new ReplaySubject(1); private _activeUserId: UserId; get activeUserId() { return this._activeUserId; @@ -42,6 +44,7 @@ export class FakeAccountService implements AccountService { accounts$ = this.accountsSubject.asObservable(); activeAccount$ = this.activeAccountSubject.asObservable(); accountActivity$ = this.accountActivitySubject.asObservable(); + accountVerifyNewDeviceLogin$ = this.accountVerifyDevicesSubject.asObservable(); get sortedUserIds$() { return this.accountActivity$.pipe( map((activity) => { @@ -52,7 +55,7 @@ export class FakeAccountService implements AccountService { }), ); } - get nextUpAccount$() { + get nextUpAccount$(): Observable { return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe( map(([accounts, activeAccount, sortedUserIds]) => { const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null); @@ -67,6 +70,11 @@ export class FakeAccountService implements AccountService { this.activeAccountSubject.next(null); this.accountActivitySubject.next(accountActivity); } + + setAccountVerifyNewDeviceLogin(userId: UserId, verifyNewDeviceLogin: boolean): Promise { + return this.mock.setAccountVerifyNewDeviceLogin(userId, verifyNewDeviceLogin); + } + setAccountActivity(userId: UserId, lastActivity: Date): Promise { this.accountActivitySubject.next({ ...this.accountActivitySubject["_buffer"][0], diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index b5105bb24ba1..9f72ccada556 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -225,9 +225,9 @@ export class FakeStateProvider implements StateProvider { async setUserState( userKeyDefinition: UserKeyDefinition, - value: T, + value: T | null, userId?: UserId, - ): Promise<[UserId, T]> { + ): Promise<[UserId, T | null]> { await this.mock.setUserState(userKeyDefinition, value, userId); if (userId) { return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)]; diff --git a/libs/common/spec/fake-state.ts b/libs/common/spec/fake-state.ts index e4b42e357b68..00b12de2eb1b 100644 --- a/libs/common/spec/fake-state.ts +++ b/libs/common/spec/fake-state.ts @@ -131,9 +131,9 @@ export class FakeSingleUserState implements SingleUserState { } async update( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options?: StateUpdateOptions, - ): Promise { + ): Promise { options = populateOptionsWithDefault(options); const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout))); const combinedDependencies = @@ -206,9 +206,9 @@ export class FakeActiveUserState implements ActiveUserState { } async update( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options?: StateUpdateOptions, - ): Promise<[UserId, T]> { + ): Promise<[UserId, T | null]> { options = populateOptionsWithDefault(options); const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout))); const combinedDependencies = diff --git a/libs/common/spec/matrix.spec.ts b/libs/common/spec/matrix.spec.ts new file mode 100644 index 000000000000..b1a5e7a96444 --- /dev/null +++ b/libs/common/spec/matrix.spec.ts @@ -0,0 +1,76 @@ +import { Matrix } from "./matrix"; + +class TestObject { + value: number = 0; + + constructor() {} + + increment() { + this.value++; + } +} + +describe("matrix", () => { + it("caches entries in a matrix properly with a single argument", () => { + const mockFunction = jest.fn(); + const getter = Matrix.autoMockMethod(mockFunction, () => new TestObject()); + + const obj = getter("test1"); + expect(obj.value).toBe(0); + + // Change the state of the object + obj.increment(); + + // Should return the same instance the second time this is called + expect(getter("test1").value).toBe(1); + + // Using the getter should not call the mock function + expect(mockFunction).not.toHaveBeenCalled(); + + const mockedFunctionReturn1 = mockFunction("test1"); + expect(mockedFunctionReturn1.value).toBe(1); + + // Totally new value + const mockedFunctionReturn2 = mockFunction("test2"); + expect(mockedFunctionReturn2.value).toBe(0); + + expect(mockFunction).toHaveBeenCalledTimes(2); + }); + + it("caches entries in matrix properly with multiple arguments", () => { + const mockFunction = jest.fn(); + + const getter = Matrix.autoMockMethod(mockFunction, () => { + return new TestObject(); + }); + + const obj = getter("test1", 4); + expect(obj.value).toBe(0); + + obj.increment(); + + expect(getter("test1", 4).value).toBe(1); + + expect(mockFunction("test1", 3).value).toBe(0); + }); + + it("should give original args in creator even if it has multiple key layers", () => { + const mockFunction = jest.fn(); + + let invoked = false; + + const getter = Matrix.autoMockMethod(mockFunction, (args) => { + expect(args).toHaveLength(3); + expect(args[0]).toBe("test"); + expect(args[1]).toBe(42); + expect(args[2]).toBe(true); + + invoked = true; + + return new TestObject(); + }); + + getter("test", 42, true); + expect(invoked).toBe(true); + }); +}); diff --git a/libs/common/spec/matrix.ts b/libs/common/spec/matrix.ts new file mode 100644 index 000000000000..e4ac9f5537f3 --- /dev/null +++ b/libs/common/spec/matrix.ts @@ -0,0 +1,115 @@ +type PickFirst = Array extends [infer First, ...unknown[]] ? First : never; + +type MatrixOrValue = Array extends [] + ? Value + : Matrix; + +type RemoveFirst = T extends [unknown, ...infer Rest] ? Rest : never; + +/** + * A matrix is intended to manage cached values for a set of method arguments. + */ +export class Matrix { + private map: Map, MatrixOrValue, TValue>> = new Map(); + + /** + * This is especially useful for methods on a service that take inputs but return Observables. + * Generally when interacting with observables in tests, you want to use a simple SubjectLike + * type to back it instead, so that you can easily `next` values to simulate an emission. + * + * @param mockFunction The function to have a Matrix based implementation added to it. + * @param creator The function to use to create the underlying value to return for the given arguments. + * @returns A "getter" function that allows you to retrieve the backing value that is used for the given arguments. + * + * @example + * ```ts + * interface MyService { + * event$(userId: UserId) => Observable + * } + * + * // Test + * const myService = mock(); + * const eventGetter = Matrix.autoMockMethod(myService.event$, (userId) => BehaviorSubject()); + * + * eventGetter("userOne").next(new UserEvent()); + * eventGetter("userTwo").next(new UserEvent()); + * ``` + * + * This replaces a more manual way of doing things like: + * + * ```ts + * const myService = mock(); + * const userOneSubject = new BehaviorSubject(); + * const userTwoSubject = new BehaviorSubject(); + * myService.event$.mockImplementation((userId) => { + * if (userId === "userOne") { + * return userOneSubject; + * } else if (userId === "userTwo") { + * return userTwoSubject; + * } + * return new BehaviorSubject(); + * }); + * + * userOneSubject.next(new UserEvent()); + * userTwoSubject.next(new UserEvent()); + * ``` + */ + static autoMockMethod( + mockFunction: jest.Mock, + creator: (args: TArgs) => TActualReturn, + ): (...args: TArgs) => TActualReturn { + const matrix = new Matrix(); + + const getter = (...args: TArgs) => { + return matrix.getOrCreateEntry(args, creator); + }; + + mockFunction.mockImplementation(getter); + + return getter; + } + + /** + * Gives the ability to get or create an entry in the matrix via the given args. + * + * @note The args are evaulated using Javascript equality so primivites work best. + * + * @param args The arguments to use to evaluate if an entry in the matrix exists already, + * or a value should be created and stored with those arguments. + * @param creator The function to call with the arguments to build a value. + * @returns The existing entry if one already exists or a new value created with the creator param. + */ + getOrCreateEntry(args: TKeys, creator: (args: TKeys) => TValue): TValue { + if (args.length === 0) { + throw new Error("Matrix is not for you."); + } + + if (args.length === 1) { + const arg = args[0] as PickFirst; + if (this.map.has(arg)) { + // Get the cached value + return this.map.get(arg) as TValue; + } else { + const value = creator(args); + // Save the value for the next time + this.map.set(arg, value as MatrixOrValue, TValue>); + return value; + } + } + + // There are for sure 2 or more args + const [first, ...rest] = args as unknown as [PickFirst, ...RemoveFirst]; + + let matrix: Matrix, TValue> | null = null; + + if (this.map.has(first)) { + // We've already created a map for this argument + matrix = this.map.get(first) as Matrix, TValue>; + } else { + matrix = new Matrix, TValue>(); + this.map.set(first, matrix as MatrixOrValue, TValue>); + } + + return matrix.getOrCreateEntry(rest, () => creator(args)); + } +} diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 997974e05819..5bd2221860b3 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -1,9 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { - CollectionRequest, CollectionAccessDetailsResponse, CollectionDetailsResponse, + CollectionRequest, CollectionResponse, } from "@bitwarden/admin-console/common"; @@ -70,6 +70,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; @@ -95,7 +96,6 @@ import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; -import { TaxRateResponse } from "../billing/models/response/tax-rate.response"; import { DeleteRecoverRequest } from "../models/request/delete-recover.request"; import { EventRequest } from "../models/request/event.request"; import { KdfRequest } from "../models/request/kdf.request"; @@ -137,7 +137,7 @@ import { OptionalCipherResponse } from "../vault/models/response/optional-cipher */ export abstract class ApiService { send: ( - method: "GET" | "POST" | "PUT" | "DELETE", + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, body: any, authed: boolean, @@ -152,7 +152,12 @@ export abstract class ApiService { | SsoTokenRequest | UserApiTokenRequest | WebAuthnLoginTokenRequest, - ) => Promise; + ) => Promise< + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse + >; refreshIdentityToken: () => Promise; getProfile: () => Promise; @@ -376,7 +381,6 @@ export abstract class ApiService { ): Promise>; deleteOrganizationConnection: (id: string) => Promise; getPlans: () => Promise>; - getTaxRates: () => Promise>; getProviderUsers: (providerId: string) => Promise>; getProviderUser: (providerId: string, id: string) => Promise; diff --git a/libs/common/src/abstractions/notifications.service.ts b/libs/common/src/abstractions/notifications.service.ts deleted file mode 100644 index 2234a5588a65..000000000000 --- a/libs/common/src/abstractions/notifications.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -export abstract class NotificationsService { - init: () => Promise; - updateConnection: (sync?: boolean) => Promise; - reconnectFromActivity: () => Promise; - disconnectFromInactivity: () => Promise; -} diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index da81f340fdae..05c214ece136 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -57,14 +57,6 @@ export function getOrganizationById(id: string) { return map((orgs) => orgs.find((o) => o.id === id)); } -/** - * Returns `true` if a user is a member of an organization (rather than only being a ProviderUser) - * @deprecated Use organizationService.organizations$ with a filter instead - */ -export function isMember(org: Organization): boolean { - return org.isMember; -} - /** * Publishes an observable stream of organizations. This service is meant to * be used widely across Bitwarden as the primary way of fetching organizations. @@ -73,41 +65,23 @@ export function isMember(org: Organization): boolean { */ export abstract class OrganizationService { /** - * Publishes state for all organizations under the active user. + * Publishes state for all organizations under the specified user. * @returns An observable list of organizations */ - organizations$: Observable; + organizations$: (userId: UserId) => Observable; // @todo Clean these up. Continuing to expand them is not recommended. // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: Observable; - /** - * @deprecated This is currently only used in the CLI, and should not be - * used in any new calls. Use get$ instead for the time being, and we'll be - * removing this method soon. See Jira for details: - * https://bitwarden.atlassian.net/browse/AC-2252. - */ - getFromState: (id: string) => Promise; + memberOrganizations$: (userId: UserId) => Observable; /** * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. */ - canManageSponsorships$: Observable; + canManageSponsorships$: (userId: UserId) => Observable; /** * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. */ - familySponsorshipAvailable$: Observable; - hasOrganizations: () => Promise; - get$: (id: string) => Observable; - get: (id: string) => Promise; - /** - * @deprecated This method is only used in key connector and will be removed soon as part of https://bitwarden.atlassian.net/browse/AC-2252. - */ - getAll: (userId?: string) => Promise; - - /** - * Publishes state for all organizations for the given user id or the active user. - */ - getAll$: (userId?: UserId) => Observable; + familySponsorshipAvailable$: (userId: UserId) => Observable; + hasOrganizations: (userId: UserId) => Observable; } /** @@ -120,20 +94,18 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio /** * Replaces state for the provided organization, or creates it if not found. * @param organization The organization state being saved. - * @param userId The userId to replace state for. Defaults to the active - * user. + * @param userId The userId to replace state for. */ - upsert: (OrganizationData: OrganizationData) => Promise; + upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; /** - * Replaces state for the entire registered organization list for the active user. + * Replaces state for the entire registered organization list for the specified user. * You probably don't want this unless you're calling from a full sync * operation or a logout. See `upsert` for creating & updating a single * organization in the state. - * @param organizations A complete list of all organization state for the active - * user. - * @param userId The userId to replace state for. Defaults to the active + * @param organizations A complete list of all organization state for the provided * user. + * @param userId The userId to replace state for. */ - replace: (organizations: { [id: string]: OrganizationData }, userId?: UserId) => Promise; + replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; } diff --git a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts b/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts deleted file mode 100644 index c25a153a068c..000000000000 --- a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { map, Observable } from "rxjs"; - -import { UserId } from "../../../types/guid"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -export function canAccessVaultTab(org: Organization): boolean { - return org.canViewAllCollections; -} - -export function canAccessSettingsTab(org: Organization): boolean { - return ( - org.isOwner || - org.canManagePolicies || - org.canManageSso || - org.canManageScim || - org.canAccessImport || - org.canAccessExport || - org.canManageDeviceApprovals - ); -} - -export function canAccessMembersTab(org: Organization): boolean { - return org.canManageUsers || org.canManageUsersPassword; -} - -export function canAccessGroupsTab(org: Organization): boolean { - return org.canManageGroups; -} - -export function canAccessReportingTab(org: Organization): boolean { - return org.canAccessReports || org.canAccessEventLogs; -} - -export function canAccessBillingTab(org: Organization): boolean { - return org.isOwner; -} - -export function canAccessOrgAdmin(org: Organization): boolean { - // Admin console can only be accessed by Owners for disabled organizations - if (!org.enabled && !org.isOwner) { - return false; - } - return ( - canAccessMembersTab(org) || - canAccessGroupsTab(org) || - canAccessReportingTab(org) || - canAccessBillingTab(org) || - canAccessSettingsTab(org) || - canAccessVaultTab(org) - ); -} - -export function getOrganizationById(id: string) { - return map((orgs) => orgs.find((o) => o.id === id)); -} - -/** - * Publishes an observable stream of organizations. This service is meant to - * be used widely across Bitwarden as the primary way of fetching organizations. - * Risky operations like updates are isolated to the - * internal extension `InternalOrganizationServiceAbstraction`. - */ -export abstract class vNextOrganizationService { - /** - * Publishes state for all organizations under the specified user. - * @returns An observable list of organizations - */ - organizations$: (userId: UserId) => Observable; - - // @todo Clean these up. Continuing to expand them is not recommended. - // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: (userId: UserId) => Observable; - /** - * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. - */ - canManageSponsorships$: (userId: UserId) => Observable; - /** - * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. - */ - familySponsorshipAvailable$: (userId: UserId) => Observable; - hasOrganizations: (userId: UserId) => Observable; -} - -/** - * Big scary buttons that **update** organization state. These should only be - * called from within admin-console scoped code. Extends the base - * `OrganizationService` for easy access to `get` calls. - * @internal - */ -export abstract class vNextInternalOrganizationServiceAbstraction extends vNextOrganizationService { - /** - * Replaces state for the provided organization, or creates it if not found. - * @param organization The organization state being saved. - * @param userId The userId to replace state for. - */ - upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; - - /** - * Replaces state for the entire registered organization list for the specified user. - * You probably don't want this unless you're calling from a full sync - * operation or a logout. See `upsert` for creating & updating a single - * organization in the state. - * @param organizations A complete list of all organization state for the provided - * user. - * @param userId The userId to replace state for. - */ - replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; -} diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index bed341115eee..4280756326ca 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -30,7 +30,7 @@ export abstract class PolicyService { * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). * @param policyType the {@link PolicyType} to search for */ - getAll$: (policyType: PolicyType, userId?: UserId) => Observable; + getAll$: (policyType: PolicyType, userId: UserId) => Observable; /** * All {@link Policy} objects for the specified user (from sync data). diff --git a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts index f348e7487dea..ffe79f0ad3b8 100644 --- a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; + import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; @@ -14,4 +16,12 @@ export class ProviderApiServiceAbstraction { request: ProviderVerifyRecoverDeleteRequest, ) => Promise; deleteProvider: (id: string) => Promise; + getProviderAddableOrganizations: (providerId: string) => Promise; + addOrganizationToProvider: ( + providerId: string, + request: { + key: string; + organizationId: string; + }, + ) => Promise; } diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index da9a82e7c5c2..5f487e1f8986 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -1,6 +1,6 @@ import { ProductTierType } from "../../../billing/enums/product-tier-type.enum"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; -import { ORGANIZATIONS } from "../../services/organization/organization.service"; +import { ORGANIZATIONS } from "../../services/organization/organization.state"; import { OrganizationData } from "./organization.data"; @@ -53,6 +53,7 @@ describe("ORGANIZATIONS state", () => { accessSecretsManager: false, limitCollectionCreation: false, limitCollectionDeletion: false, + limitItemDeletion: false, allowAdminAccessToAllCollectionItems: false, familySponsorshipLastSyncDate: new Date(), userIsManagedByOrganization: false, diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index 8ec84b5fd09b..b81d06e6367a 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -56,6 +56,7 @@ export class OrganizationData { accessSecretsManager: boolean; limitCollectionCreation: boolean; limitCollectionDeletion: boolean; + limitItemDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; useRiskInsights: boolean; @@ -117,6 +118,7 @@ export class OrganizationData { this.accessSecretsManager = response.accessSecretsManager; this.limitCollectionCreation = response.limitCollectionCreation; this.limitCollectionDeletion = response.limitCollectionDeletion; + this.limitItemDeletion = response.limitItemDeletion; this.allowAdminAccessToAllCollectionItems = response.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = response.userIsManagedByOrganization; this.useRiskInsights = response.useRiskInsights; diff --git a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts index 1f8c4e8c42da..5f8fe3349b6d 100644 --- a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts +++ b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts @@ -1,4 +1,4 @@ -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { OrgKey, UserPrivateKey } from "../../../types/key"; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 9dcc9f0752c0..6f7ff561f04f 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -76,6 +76,12 @@ export class Organization { /** * Refers to the ability for an owner/admin to access all collection items, regardless of assigned collections */ + limitItemDeletion: boolean; + /** + * Refers to the ability to limit delete permission of collection items. + * If set to true, members can only delete items when they have a Can Manage permission over the collection. + * If set to false, members can delete items when they have a Can Manage OR Can Edit permission over the collection. + */ allowAdminAccessToAllCollectionItems: boolean; /** * Indicates if this organization manages the user. @@ -138,6 +144,7 @@ export class Organization { this.accessSecretsManager = obj.accessSecretsManager; this.limitCollectionCreation = obj.limitCollectionCreation; this.limitCollectionDeletion = obj.limitCollectionDeletion; + this.limitItemDeletion = obj.limitItemDeletion; this.allowAdminAccessToAllCollectionItems = obj.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = obj.userIsManagedByOrganization; this.useRiskInsights = obj.useRiskInsights; diff --git a/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts b/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts index 23c39376d71c..2545a7255986 100644 --- a/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts +++ b/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts @@ -3,5 +3,6 @@ export class OrganizationCollectionManagementUpdateRequest { limitCollectionCreation: boolean; limitCollectionDeletion: boolean; + limitItemDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; } diff --git a/libs/common/src/admin-console/models/response/addable-organization.response.ts b/libs/common/src/admin-console/models/response/addable-organization.response.ts new file mode 100644 index 000000000000..74ae5f456901 --- /dev/null +++ b/libs/common/src/admin-console/models/response/addable-organization.response.ts @@ -0,0 +1,18 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class AddableOrganizationResponse extends BaseResponse { + id: string; + plan: string; + name: string; + seats: number; + disabled: boolean; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("id"); + this.plan = this.getResponseProperty("plan"); + this.name = this.getResponseProperty("name"); + this.seats = this.getResponseProperty("seats"); + this.disabled = this.getResponseProperty("disabled"); + } +} diff --git a/libs/common/src/admin-console/models/response/organization.response.ts b/libs/common/src/admin-console/models/response/organization.response.ts index fd54ff128b65..235ea2f8d962 100644 --- a/libs/common/src/admin-console/models/response/organization.response.ts +++ b/libs/common/src/admin-console/models/response/organization.response.ts @@ -36,6 +36,7 @@ export class OrganizationResponse extends BaseResponse { maxAutoscaleSmServiceAccounts?: number; limitCollectionCreation: boolean; limitCollectionDeletion: boolean; + limitItemDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; useRiskInsights: boolean; @@ -75,6 +76,7 @@ export class OrganizationResponse extends BaseResponse { this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts"); this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation"); this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion"); + this.limitItemDeletion = this.getResponseProperty("LimitItemDeletion"); this.allowAdminAccessToAllCollectionItems = this.getResponseProperty( "AllowAdminAccessToAllCollectionItems", ); diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index 9c4b8885ab8c..5e37cfc4c5c5 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -51,6 +51,7 @@ export class ProfileOrganizationResponse extends BaseResponse { accessSecretsManager: boolean; limitCollectionCreation: boolean; limitCollectionDeletion: boolean; + limitItemDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; useRiskInsights: boolean; @@ -114,6 +115,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager"); this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation"); this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion"); + this.limitItemDeletion = this.getResponseProperty("LimitItemDeletion"); this.allowAdminAccessToAllCollectionItems = this.getResponseProperty( "AllowAdminAccessToAllCollectionItems", ); diff --git a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts b/libs/common/src/admin-console/services/organization/default-organization.service.spec.ts similarity index 96% rename from libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts rename to libs/common/src/admin-console/services/organization/default-organization.service.spec.ts index 9e2ea3a45992..41c89c0e41a8 100644 --- a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts +++ b/libs/common/src/admin-console/services/organization/default-organization.service.spec.ts @@ -6,11 +6,11 @@ import { OrganizationId, UserId } from "../../../types/guid"; import { OrganizationData } from "../../models/data/organization.data"; import { Organization } from "../../models/domain/organization"; -import { DefaultvNextOrganizationService } from "./default-vnext-organization.service"; -import { ORGANIZATIONS } from "./vnext-organization.state"; +import { DefaultOrganizationService } from "./default-organization.service"; +import { ORGANIZATIONS } from "./organization.state"; describe("OrganizationService", () => { - let organizationService: DefaultvNextOrganizationService; + let organizationService: DefaultOrganizationService; const fakeUserId = Utils.newGuid() as UserId; let fakeStateProvider: FakeStateProvider; @@ -86,7 +86,7 @@ describe("OrganizationService", () => { beforeEach(async () => { fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(fakeUserId)); - organizationService = new DefaultvNextOrganizationService(fakeStateProvider); + organizationService = new DefaultOrganizationService(fakeStateProvider); }); describe("canManageSponsorships", () => { diff --git a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts b/libs/common/src/admin-console/services/organization/default-organization.service.ts similarity index 92% rename from libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts rename to libs/common/src/admin-console/services/organization/default-organization.service.ts index 8b73c271daff..e78136455fd3 100644 --- a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts +++ b/libs/common/src/admin-console/services/organization/default-organization.service.ts @@ -4,11 +4,11 @@ import { map, Observable } from "rxjs"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; -import { vNextInternalOrganizationServiceAbstraction } from "../../abstractions/organization/vnext.organization.service"; +import { InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../models/data/organization.data"; import { Organization } from "../../models/domain/organization"; -import { ORGANIZATIONS } from "./vnext-organization.state"; +import { ORGANIZATIONS } from "./organization.state"; /** * Filter out organizations from an observable that __do not__ offer a @@ -41,9 +41,7 @@ function mapToBooleanHasAnyOrganizations() { return map((orgs) => orgs.length > 0); } -export class DefaultvNextOrganizationService - implements vNextInternalOrganizationServiceAbstraction -{ +export class DefaultOrganizationService implements InternalOrganizationServiceAbstraction { memberOrganizations$(userId: UserId): Observable { return this.organizations$(userId).pipe(mapToExcludeProviderOrganizations()); } diff --git a/libs/common/src/admin-console/services/organization/organization.service.spec.ts b/libs/common/src/admin-console/services/organization/organization.service.spec.ts deleted file mode 100644 index 6d2525966bc1..000000000000 --- a/libs/common/src/admin-console/services/organization/organization.service.spec.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { firstValueFrom } from "rxjs"; - -import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; -import { Utils } from "../../../platform/misc/utils"; -import { OrganizationId, UserId } from "../../../types/guid"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -import { OrganizationService, ORGANIZATIONS } from "./organization.service"; - -describe("OrganizationService", () => { - let organizationService: OrganizationService; - - const fakeUserId = Utils.newGuid() as UserId; - let fakeAccountService: FakeAccountService; - let fakeStateProvider: FakeStateProvider; - let fakeActiveUserState: FakeActiveUserState>; - - /** - * It is easier to read arrays than records in code, but we store a record - * in state. This helper methods lets us build organization arrays in tests - * and easily map them to records before storing them in state. - */ - function arrayToRecord(input: OrganizationData[]): Record { - if (input == null) { - return undefined; - } - return Object.fromEntries(input?.map((i) => [i.id, i])); - } - - /** - * There are a few assertions in this spec that check for array equality - * but want to ignore a specific index that _should_ be different. This - * function takes two arrays, and an index. It checks for equality of the - * arrays, but splices out the specified index from both arrays first. - */ - function expectIsEqualExceptForIndex(x: any[], y: any[], indexToExclude: number) { - // Clone the arrays to avoid modifying the reference values - const a = [...x]; - const b = [...y]; - delete a[indexToExclude]; - delete b[indexToExclude]; - expect(a).toEqual(b); - } - - /** - * Builds a simple mock `OrganizationData[]` array that can be used in tests - * to populate state. - * @param count The number of organizations to populate the list with. The - * function returns undefined if this is less than 1. The default value is 1. - * @param suffix A string to append to data fields on each organization. - * This defaults to the index of the organization in the list. - * @returns an `OrganizationData[]` array that can be used to populate - * stateProvider. - */ - function buildMockOrganizations(count = 1, suffix?: string): OrganizationData[] { - if (count < 1) { - return undefined; - } - - function buildMockOrganization(id: OrganizationId, name: string, identifier: string) { - const data = new OrganizationData({} as any, {} as any); - data.id = id; - data.name = name; - data.identifier = identifier; - - return data; - } - - const mockOrganizations = []; - for (let i = 0; i < count; i++) { - const s = suffix ? suffix + i.toString() : i.toString(); - mockOrganizations.push( - buildMockOrganization(("org" + s) as OrganizationId, "org" + s, "orgIdentifier" + s), - ); - } - - return mockOrganizations; - } - - /** - * `OrganizationService` deals with multiple accounts at times. This helper - * function can be used to add a new non-active account to the test data. - * This function is **not** needed to handle creation of the first account, - * as that is handled by the `FakeAccountService` in `mockAccountServiceWith()` - * @returns The `UserId` of the newly created state account and the mock data - * created for them as an `Organization[]`. - */ - async function addNonActiveAccountToStateProvider(): Promise<[UserId, OrganizationData[]]> { - const nonActiveUserId = Utils.newGuid() as UserId; - - const mockOrganizations = buildMockOrganizations(10); - const fakeNonActiveUserState = fakeStateProvider.singleUser.getFake( - nonActiveUserId, - ORGANIZATIONS, - ); - fakeNonActiveUserState.nextState(arrayToRecord(mockOrganizations)); - - return [nonActiveUserId, mockOrganizations]; - } - - beforeEach(async () => { - fakeAccountService = mockAccountServiceWith(fakeUserId); - fakeStateProvider = new FakeStateProvider(fakeAccountService); - fakeActiveUserState = fakeStateProvider.activeUser.getFake(ORGANIZATIONS); - organizationService = new OrganizationService(fakeStateProvider); - }); - - it("getAll", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const orgs = await organizationService.getAll(); - expect(orgs).toHaveLength(1); - const org = orgs[0]; - expect(org).toEqual(new Organization(mockData[0])); - }); - - describe("canManageSponsorships", () => { - it("can because one is available", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipAvailable = true; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(true); - }); - - it("can because one is used", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipFriendlyName = "Something"; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(true); - }); - - it("can not because one isn't available or taken", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipFriendlyName = null; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(false); - }); - }); - - describe("get", () => { - it("exists", async () => { - const mockData = buildMockOrganizations(1); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await organizationService.get(mockData[0].id); - expect(result).toEqual(new Organization(mockData[0])); - }); - - it("does not exist", async () => { - const result = await organizationService.get("this-org-does-not-exist"); - expect(result).toBe(undefined); - }); - }); - - describe("organizations$", () => { - describe("null checking behavior", () => { - it("publishes an empty array if organizations in state = undefined", async () => { - const mockData: OrganizationData[] = undefined; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - - it("publishes an empty array if organizations in state = null", async () => { - const mockData: OrganizationData[] = null; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - - it("publishes an empty array if organizations in state = []", async () => { - const mockData: OrganizationData[] = []; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - }); - - describe("parameter handling & returns", () => { - it("publishes all organizations for the active user by default", async () => { - const mockData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual(mockData); - }); - - it("can be used to publish the organizations of a non active user if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserState"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, nonActiveUserMockOrganizations] = - await addNonActiveAccountToStateProvider(); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - - expect(result).toEqual(nonActiveUserMockOrganizations); - expect(result).not.toEqual(await firstValueFrom(organizationService.organizations$)); - }); - }); - }); - - describe("upsert()", () => { - it("can create the organization list if necassary", async () => { - // Notice that no default state is provided in this test, so the list in - // `stateProvider` will be null when the `upsert` method is called. - const mockData = buildMockOrganizations(); - await organizationService.upsert(mockData[0]); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual(mockData.map((x) => new Organization(x))); - }); - - it("updates an organization that already exists in state, defaulting to the active user", async () => { - const mockData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const indexToUpdate = 5; - const anUpdatedOrganization = { - ...buildMockOrganizations(1, "UPDATED").pop(), - id: mockData[indexToUpdate].id, - }; - await organizationService.upsert(anUpdatedOrganization); - const result = await firstValueFrom(organizationService.organizations$); - expect(result[indexToUpdate]).not.toEqual(new Organization(mockData[indexToUpdate])); - expect(result[indexToUpdate].id).toEqual(new Organization(mockData[indexToUpdate]).id); - expectIsEqualExceptForIndex( - result, - mockData.map((x) => new Organization(x)), - indexToUpdate, - ); - }); - - it("can also update an organization in state for a non-active user, if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserOrganizations"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, nonActiveUserMockOrganizations] = - await addNonActiveAccountToStateProvider(); - const indexToUpdate = 5; - const anUpdatedOrganization = { - ...buildMockOrganizations(1, "UPDATED").pop(), - id: nonActiveUserMockOrganizations[indexToUpdate].id, - }; - - await organizationService.upsert(anUpdatedOrganization, nonActiveUserId); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - - expect(result[indexToUpdate]).not.toEqual( - new Organization(nonActiveUserMockOrganizations[indexToUpdate]), - ); - expect(result[indexToUpdate].id).toEqual( - new Organization(nonActiveUserMockOrganizations[indexToUpdate]).id, - ); - expectIsEqualExceptForIndex( - result, - nonActiveUserMockOrganizations.map((x) => new Organization(x)), - indexToUpdate, - ); - - // Just to be safe, lets make sure the active user didn't get updated - // at all - const activeUserState = await firstValueFrom(organizationService.organizations$); - expect(activeUserState).toEqual(activeUserMockData.map((x) => new Organization(x))); - expect(activeUserState).not.toEqual(result); - }); - }); - - describe("replace()", () => { - it("replaces the entire organization list in state, defaulting to the active user", async () => { - const originalData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(originalData)); - - const newData = buildMockOrganizations(10, "newData"); - await organizationService.replace(arrayToRecord(newData)); - - const result = await firstValueFrom(organizationService.organizations$); - - expect(result).toEqual(newData); - expect(result).not.toEqual(originalData); - }); - - // This is more or less a test for logouts - it("can replace state with null", async () => { - const originalData = buildMockOrganizations(2); - fakeActiveUserState.nextState(arrayToRecord(originalData)); - await organizationService.replace(null); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - expect(result).not.toEqual(originalData); - }); - - it("can also replace state for a non-active user, if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserOrganizations"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, originalOrganizations] = await addNonActiveAccountToStateProvider(); - const newData = buildMockOrganizations(10, "newData"); - - await organizationService.replace(arrayToRecord(newData), nonActiveUserId); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - expect(result).toEqual(newData); - expect(result).not.toEqual(originalOrganizations); - - // Just to be safe, lets make sure the active user didn't get updated - // at all - const activeUserState = await firstValueFrom(organizationService.organizations$); - expect(activeUserState).toEqual(activeUserMockData.map((x) => new Organization(x))); - expect(activeUserState).not.toEqual(result); - }); - }); -}); diff --git a/libs/common/src/admin-console/services/organization/organization.service.ts b/libs/common/src/admin-console/services/organization/organization.service.ts deleted file mode 100644 index 49e906bdac22..000000000000 --- a/libs/common/src/admin-console/services/organization/organization.service.ts +++ /dev/null @@ -1,160 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { map, Observable, firstValueFrom } from "rxjs"; -import { Jsonify } from "type-fest"; - -import { ORGANIZATIONS_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -/** - * The `KeyDefinition` for accessing organization lists in application state. - * @todo Ideally this wouldn't require a `fromJSON()` call, but `OrganizationData` - * has some properties that contain functions. This should probably get - * cleaned up. - */ -export const ORGANIZATIONS = UserKeyDefinition.record( - ORGANIZATIONS_DISK, - "organizations", - { - deserializer: (obj: Jsonify) => OrganizationData.fromJSON(obj), - clearOn: ["logout"], - }, -); - -/** - * Filter out organizations from an observable that __do not__ offer a - * families-for-enterprise sponsorship to members. - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToExcludeOrganizationsWithoutFamilySponsorshipSupport() { - return map((orgs) => orgs.filter((o) => o.canManageSponsorships)); -} - -/** - * Filter out organizations from an observable that the organization user - * __is not__ a direct member of. This will exclude organizations only - * accessible as a provider. - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToExcludeProviderOrganizations() { - return map((orgs) => orgs.filter((o) => o.isMember)); -} - -/** - * Map an observable stream of organizations down to a boolean indicating - * if any organizations exist (`orgs.length > 0`). - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToBooleanHasAnyOrganizations() { - return map((orgs) => orgs.length > 0); -} - -/** - * Map an observable stream of organizations down to a single organization. - * @param `organizationId` The ID of the organization you'd like to subscribe to - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToSingleOrganization(organizationId: string) { - return map((orgs) => orgs?.find((o) => o.id === organizationId)); -} - -export class OrganizationService implements InternalOrganizationServiceAbstraction { - organizations$: Observable = this.getOrganizationsFromState$(); - memberOrganizations$: Observable = this.organizations$.pipe( - mapToExcludeProviderOrganizations(), - ); - - constructor(private stateProvider: StateProvider) {} - - get$(id: string): Observable { - return this.organizations$.pipe(mapToSingleOrganization(id)); - } - - getAll$(userId?: UserId): Observable { - return this.getOrganizationsFromState$(userId); - } - - async getAll(userId?: string): Promise { - return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId)); - } - - canManageSponsorships$ = this.organizations$.pipe( - mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(), - mapToBooleanHasAnyOrganizations(), - ); - - familySponsorshipAvailable$ = this.organizations$.pipe( - map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)), - ); - - async hasOrganizations(): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations())); - } - - async upsert(organization: OrganizationData, userId?: UserId): Promise { - await this.stateFor(userId).update((existingOrganizations) => { - const organizations = existingOrganizations ?? {}; - organizations[organization.id] = organization; - return organizations; - }); - } - - async get(id: string): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToSingleOrganization(id))); - } - - /** - * @deprecated For the CLI only - * @param id id of the organization - */ - async getFromState(id: string): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToSingleOrganization(id))); - } - - async replace(organizations: { [id: string]: OrganizationData }, userId?: UserId): Promise { - await this.stateFor(userId).update(() => organizations); - } - - // Ideally this method would be renamed to organizations$() and the - // $organizations observable as it stands would be removed. This will - // require updates to callers, and so this method exists as a temporary - // workaround until we have time & a plan to update callers. - // - // It can be thought of as "organizations$ but with a userId option". - private getOrganizationsFromState$(userId?: UserId): Observable { - return this.stateFor(userId).state$.pipe(this.mapOrganizationRecordToArray()); - } - - /** - * Accepts a record of `OrganizationData`, which is how we store the - * organization list as a JSON object on disk, to an array of - * `Organization`, which is how the data is published to callers of the - * service. - * @returns a function that can be used to pipe organization data from - * stored state to an exposed object easily consumable by others. - */ - private mapOrganizationRecordToArray() { - return map, Organization[]>((orgs) => - Object.values(orgs ?? {})?.map((o) => new Organization(o)), - ); - } - - /** - * Fetches the organization list from on disk state for the specified user. - * @param userId the user ID to fetch the organization list for. Defaults to - * the currently active user. - * @returns an observable of organization state as it is stored on disk. - */ - private stateFor(userId?: UserId) { - return userId - ? this.stateProvider.getUser(userId, ORGANIZATIONS) - : this.stateProvider.getActive(ORGANIZATIONS); - } -} diff --git a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts b/libs/common/src/admin-console/services/organization/organization.state.ts similarity index 100% rename from libs/common/src/admin-console/services/organization/vnext-organization.state.ts rename to libs/common/src/admin-console/services/organization/organization.state.ts diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts index d9802db9e38f..12b57f1b4f76 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -2,8 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; -import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; +import { FakeActiveUserState, FakeSingleUserState } from "../../../../spec/fake-state"; import { OrganizationUserStatusType, OrganizationUserType, @@ -18,12 +17,14 @@ import { Policy } from "../../../admin-console/models/domain/policy"; import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain/reset-password-policy-options"; import { POLICIES, PolicyService } from "../../../admin-console/services/policy/policy.service"; import { PolicyId, UserId } from "../../../types/guid"; +import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; describe("PolicyService", () => { const userId = "userId" as UserId; let stateProvider: FakeStateProvider; let organizationService: MockProxy; let activeUserState: FakeActiveUserState>; + let singleUserState: FakeSingleUserState>; let policyService: PolicyService; @@ -33,6 +34,7 @@ describe("PolicyService", () => { organizationService = mock(); activeUserState = stateProvider.activeUser.getFake(POLICIES); + singleUserState = stateProvider.singleUser.getFake(activeUserState.userId, POLICIES); const organizations$ = of([ // User @@ -56,9 +58,7 @@ describe("PolicyService", () => { organization("org6", true, true, OrganizationUserStatusType.Confirmed, true), ]); - organizationService.organizations$ = organizations$; - - organizationService.getAll$.mockReturnValue(organizations$); + organizationService.organizations$.mockReturnValue(organizations$); policyService = new PolicyService(stateProvider, organizationService); }); @@ -196,7 +196,7 @@ describe("PolicyService", () => { describe("getResetPasswordPolicyOptions", () => { it("default", async () => { - const result = policyService.getResetPasswordPolicyOptions(null, null); + const result = policyService.getResetPasswordPolicyOptions([], ""); expect(result).toEqual([new ResetPasswordPolicyOptions(), false]); }); @@ -297,7 +297,7 @@ describe("PolicyService", () => { describe("getAll$", () => { it("returns the specified PolicyTypes", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -307,7 +307,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -333,7 +333,7 @@ describe("PolicyService", () => { }); it("does not return disabled policies", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -343,7 +343,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -363,7 +363,7 @@ describe("PolicyService", () => { }); it("does not return policies that do not apply to the user because the user's role is exempt", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -373,7 +373,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -393,7 +393,7 @@ describe("PolicyService", () => { }); it("does not return policies for organizations that do not use policies", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -403,7 +403,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index 7a04ba38aa74..3378d2021ef3 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable, of } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { UserKeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; import { PolicyId, UserId } from "../../../types/guid"; @@ -39,7 +39,11 @@ export class PolicyService implements InternalPolicyServiceAbstraction { map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.organizations$]).pipe( + const organizations$ = this.stateProvider.activeUserId$.pipe( + switchMap((userId) => this.organizationService.organizations$(userId)), + ); + + return combineLatest([filteredPolicies$, organizations$]).pipe( map( ([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)?.at(0) ?? null, @@ -47,13 +51,13 @@ export class PolicyService implements InternalPolicyServiceAbstraction { ); } - getAll$(policyType: PolicyType, userId?: UserId) { + getAll$(policyType: PolicyType, userId: UserId) { const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe( map((policyData) => policyRecordToArray(policyData)), map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.getAll$(userId)]).pipe( + return combineLatest([filteredPolicies$, this.organizationService.organizations$(userId)]).pipe( map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)), ); } diff --git a/libs/common/src/admin-console/services/provider/provider-api.service.ts b/libs/common/src/admin-console/services/provider/provider-api.service.ts index 2ee921393ff6..dc82ec011f4a 100644 --- a/libs/common/src/admin-console/services/provider/provider-api.service.ts +++ b/libs/common/src/admin-console/services/provider/provider-api.service.ts @@ -1,3 +1,5 @@ +import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; + import { ApiService } from "../../../abstractions/api.service"; import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; @@ -44,4 +46,34 @@ export class ProviderApiService implements ProviderApiServiceAbstraction { async deleteProvider(id: string): Promise { await this.apiService.send("DELETE", "/providers/" + id, null, true, false); } + + async getProviderAddableOrganizations( + providerId: string, + ): Promise { + const response = await this.apiService.send( + "GET", + "/providers/" + providerId + "/clients/addable", + null, + true, + true, + ); + + return response.map((data: any) => new AddableOrganizationResponse(data)); + } + + addOrganizationToProvider( + providerId: string, + request: { + key: string; + organizationId: string; + }, + ): Promise { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/clients/existing", + request, + true, + false, + ); + } } diff --git a/libs/common/src/auth/abstractions/account-api.service.ts b/libs/common/src/auth/abstractions/account-api.service.ts index 78fbb2cf8820..61fdd4f9d68e 100644 --- a/libs/common/src/auth/abstractions/account-api.service.ts +++ b/libs/common/src/auth/abstractions/account-api.service.ts @@ -1,6 +1,7 @@ import { RegisterFinishRequest } from "../models/request/registration/register-finish.request"; import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request"; import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request"; +import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request"; import { Verification } from "../types/verification"; export abstract class AccountApiService { @@ -18,7 +19,7 @@ export abstract class AccountApiService { * * @param request - The request object containing * information needed to send the verification email, such as the user's email address. - * @returns A promise that resolves to a string tokencontaining the user's encrypted + * @returns A promise that resolves to a string token containing the user's encrypted * information which must be submitted to complete registration or `null` if * email verification is enabled (users must get the token by clicking a * link in the email that will be sent to them). @@ -33,7 +34,7 @@ export abstract class AccountApiService { * * @param request - The request object containing the email verification token and the * user's email address (which is required to validate the token) - * @returns A promise that resolves when the event is logged on the server succcessfully or a bad + * @returns A promise that resolves when the event is logged on the server successfully or a bad * request if the token is invalid for any reason. */ abstract registerVerificationEmailClicked( @@ -50,4 +51,15 @@ export abstract class AccountApiService { * registration process is successfully completed. */ abstract registerFinish(request: RegisterFinishRequest): Promise; + + /** + * Sets the [dbo].[User].[VerifyDevices] flag to true or false. + * + * @param request - The request object is a SecretVerificationRequest extension + * that also contains the boolean value that the VerifyDevices property is being + * set to. + * @returns A promise that resolves when the process is successfully completed or + * a bad request if secret verification fails. + */ + abstract setVerifyDevices(request: SetVerifyDevicesRequest): Promise; } diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index 094e005e656e..1686eefda062 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -43,6 +43,8 @@ export abstract class AccountService { * Observable of the last activity time for each account. */ accountActivity$: Observable>; + /** Observable of the new device login verification property for the account. */ + accountVerifyNewDeviceLogin$: Observable; /** Account list in order of descending recency */ sortedUserIds$: Observable; /** Next account that is not the current active account */ @@ -73,6 +75,15 @@ export abstract class AccountService { * @param emailVerified */ abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise; + /** + * updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account. + * @param userId + * @param VerifyNewDeviceLogin + */ + abstract setAccountVerifyNewDeviceLogin( + userId: UserId, + verifyNewDeviceLogin: boolean, + ): Promise; /** * Updates the `activeAccount$` observable with the new active account. * @param userId diff --git a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts index 13963b03bea2..24a5d4e84137 100644 --- a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts @@ -9,7 +9,18 @@ import { DeviceKey, UserKey } from "../../types/key"; import { DeviceResponse } from "./devices/responses/device.response"; export abstract class DeviceTrustServiceAbstraction { + /** + * @deprecated - use supportsDeviceTrustByUserId instead as active user state is being deprecated + * by Platform + * @description Checks if the device trust feature is supported for the active user. + */ supportsDeviceTrust$: Observable; + + /** + * @description Checks if the device trust feature is supported for the given user. + */ + supportsDeviceTrustByUserId$: (userId: UserId) => Observable; + /** * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset diff --git a/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts b/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts index 3f3731e0e1bd..b30739d94a88 100644 --- a/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts @@ -1,5 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore +import { UserId } from "@bitwarden/common/types/guid"; + export abstract class SsoLoginServiceAbstraction { /** * Gets the code verifier used for SSO. @@ -11,7 +11,7 @@ export abstract class SsoLoginServiceAbstraction { * @see https://datatracker.ietf.org/doc/html/rfc7636 * @returns The code verifier used for SSO. */ - getCodeVerifier: () => Promise; + abstract getCodeVerifier: () => Promise; /** * Sets the code verifier used for SSO. * @@ -21,7 +21,7 @@ export abstract class SsoLoginServiceAbstraction { * and verify it matches the one sent in the request for the `authorization_code`. * @see https://datatracker.ietf.org/doc/html/rfc7636 */ - setCodeVerifier: (codeVerifier: string) => Promise; + abstract setCodeVerifier: (codeVerifier: string) => Promise; /** * Gets the value of the SSO state. * @@ -31,7 +31,7 @@ export abstract class SsoLoginServiceAbstraction { * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1 * @returns The SSO state. */ - getSsoState: () => Promise; + abstract getSsoState: () => Promise; /** * Sets the value of the SSO state. * @@ -40,7 +40,7 @@ export abstract class SsoLoginServiceAbstraction { * returns the `state` in the callback and the client verifies that the value returned matches the value sent. * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1 */ - setSsoState: (ssoState: string) => Promise; + abstract setSsoState: (ssoState: string) => Promise; /** * Gets the value of the user's organization sso identifier. * @@ -48,20 +48,20 @@ export abstract class SsoLoginServiceAbstraction { * Do not use this value outside of the SSO login flow. * @returns The user's organization identifier. */ - getOrganizationSsoIdentifier: () => Promise; + abstract getOrganizationSsoIdentifier: () => Promise; /** * Sets the value of the user's organization sso identifier. * * This should only be used during the SSO flow to identify the organization that the user is attempting to log in to. * Do not use this value outside of the SSO login flow. */ - setOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise; + abstract setOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise; /** * Gets the user's email. * Note: This should only be used during the SSO flow to identify the user that is attempting to log in. * @returns The user's email. */ - getSsoEmail: () => Promise; + abstract getSsoEmail: () => Promise; /** * Sets the user's email. * Note: This should only be used during the SSO flow to identify the user that is attempting to log in. @@ -69,17 +69,21 @@ export abstract class SsoLoginServiceAbstraction { * @returns A promise that resolves when the email has been set. * */ - setSsoEmail: (email: string) => Promise; + abstract setSsoEmail: (email: string) => Promise; /** * Gets the value of the active user's organization sso identifier. * * This should only be used post successful SSO login once the user is initialized. + * @param userId The user id for retrieving the org identifier state. */ - getActiveUserOrganizationSsoIdentifier: () => Promise; + abstract getActiveUserOrganizationSsoIdentifier: (userId: UserId) => Promise; /** * Sets the value of the active user's organization sso identifier. * * This should only be used post successful SSO login once the user is initialized. */ - setActiveUserOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise; + abstract setActiveUserOrganizationSsoIdentifier: ( + organizationIdentifier: string, + userId: UserId | undefined, + ) => Promise; } diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 1c176c2b84b3..fdc8c963a1b6 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -22,6 +22,7 @@ export class AuthResult { ssoEmail2FaSessionToken?: string; email: string; requiresEncryptionKeyMigration: boolean; + requiresDeviceVerification: boolean; get requiresCaptcha() { return !Utils.isNullOrWhitespace(this.captchaSiteKey); diff --git a/libs/common/src/auth/models/request/identity-token/password-token.request.ts b/libs/common/src/auth/models/request/identity-token/password-token.request.ts index 456e058a2347..3fe466e143b3 100644 --- a/libs/common/src/auth/models/request/identity-token/password-token.request.ts +++ b/libs/common/src/auth/models/request/identity-token/password-token.request.ts @@ -13,6 +13,7 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect public captchaResponse: string, protected twoFactor: TokenTwoFactorRequest, device?: DeviceRequest, + public newDeviceOtp?: string, ) { super(twoFactor, device); } @@ -28,6 +29,10 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect obj.captchaResponse = this.captchaResponse; } + if (this.newDeviceOtp) { + obj.newDeviceOtp = this.newDeviceOtp; + } + return obj; } diff --git a/libs/common/src/auth/models/request/set-verify-devices.request.ts b/libs/common/src/auth/models/request/set-verify-devices.request.ts new file mode 100644 index 000000000000..4835e9f09cce --- /dev/null +++ b/libs/common/src/auth/models/request/set-verify-devices.request.ts @@ -0,0 +1,8 @@ +import { SecretVerificationRequest } from "./secret-verification.request"; + +export class SetVerifyDevicesRequest extends SecretVerificationRequest { + /** + * This is the input for a user update that controls [dbo].[Users].[VerifyDevices] + */ + verifyDevices!: boolean; +} diff --git a/libs/common/src/auth/models/response/identity-device-verification.response.ts b/libs/common/src/auth/models/response/identity-device-verification.response.ts new file mode 100644 index 000000000000..b45f47e99e1b --- /dev/null +++ b/libs/common/src/auth/models/response/identity-device-verification.response.ts @@ -0,0 +1,13 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class IdentityDeviceVerificationResponse extends BaseResponse { + deviceVerified: boolean; + captchaToken: string; + + constructor(response: any) { + super(response); + this.deviceVerified = this.getResponseProperty("DeviceVerified") ?? false; + + this.captchaToken = this.getResponseProperty("CaptchaBypassToken"); + } +} diff --git a/libs/common/src/auth/models/response/identity-response.ts b/libs/common/src/auth/models/response/identity-response.ts new file mode 100644 index 000000000000..26503a9cc2f0 --- /dev/null +++ b/libs/common/src/auth/models/response/identity-response.ts @@ -0,0 +1,8 @@ +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; +import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; +import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; + +export type IdentityResponse = + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse; diff --git a/libs/common/src/auth/services/account-api.service.ts b/libs/common/src/auth/services/account-api.service.ts index e10b0686f616..0347694c465d 100644 --- a/libs/common/src/auth/services/account-api.service.ts +++ b/libs/common/src/auth/services/account-api.service.ts @@ -10,6 +10,7 @@ import { UserVerificationService } from "../abstractions/user-verification/user- import { RegisterFinishRequest } from "../models/request/registration/register-finish.request"; import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request"; import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request"; +import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request"; import { Verification } from "../types/verification"; export class AccountApiServiceImplementation implements AccountApiService { @@ -102,4 +103,21 @@ export class AccountApiServiceImplementation implements AccountApiService { throw e; } } + + async setVerifyDevices(request: SetVerifyDevicesRequest): Promise { + try { + const response = await this.apiService.send( + "POST", + "/accounts/verify-devices", + request, + true, + true, + ); + + return response; + } catch (e: unknown) { + this.logService.error(e); + throw e; + } + } } diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index 227949156eec..3fc470020836 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -7,7 +7,10 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; import { FakeGlobalState } from "../../../spec/fake-state"; -import { FakeGlobalStateProvider } from "../../../spec/fake-state-provider"; +import { + FakeGlobalStateProvider, + FakeSingleUserStateProvider, +} from "../../../spec/fake-state-provider"; import { trackEmissions } from "../../../spec/utils"; import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; @@ -19,6 +22,7 @@ import { ACCOUNT_ACCOUNTS, ACCOUNT_ACTIVE_ACCOUNT_ID, ACCOUNT_ACTIVITY, + ACCOUNT_VERIFY_NEW_DEVICE_LOGIN, AccountServiceImplementation, } from "./account.service"; @@ -66,9 +70,11 @@ describe("accountService", () => { let messagingService: MockProxy; let logService: MockProxy; let globalStateProvider: FakeGlobalStateProvider; + let singleUserStateProvider: FakeSingleUserStateProvider; let sut: AccountServiceImplementation; let accountsState: FakeGlobalState>; let activeAccountIdState: FakeGlobalState; + let accountActivityState: FakeGlobalState>; const userId = Utils.newGuid() as UserId; const userInfo = { email: "email", name: "name", emailVerified: true }; @@ -76,11 +82,18 @@ describe("accountService", () => { messagingService = mock(); logService = mock(); globalStateProvider = new FakeGlobalStateProvider(); + singleUserStateProvider = new FakeSingleUserStateProvider(); - sut = new AccountServiceImplementation(messagingService, logService, globalStateProvider); + sut = new AccountServiceImplementation( + messagingService, + logService, + globalStateProvider, + singleUserStateProvider, + ); accountsState = globalStateProvider.getFake(ACCOUNT_ACCOUNTS); activeAccountIdState = globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID); + accountActivityState = globalStateProvider.getFake(ACCOUNT_ACTIVITY); }); afterEach(() => { @@ -126,6 +139,22 @@ describe("accountService", () => { }); }); + describe("accountsVerifyNewDeviceLogin$", () => { + it("returns expected value", async () => { + // Arrange + const expected = true; + // we need to set this state since it is how we initialize the VerifyNewDeviceLogin$ + activeAccountIdState.stateSubject.next(userId); + singleUserStateProvider.getFake(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).nextState(expected); + + // Act + const result = await firstValueFrom(sut.accountVerifyNewDeviceLogin$); + + // Assert + expect(result).toEqual(expected); + }); + }); + describe("addAccount", () => { it("should emit the new account", async () => { await sut.addAccount(userId, userInfo); @@ -224,6 +253,33 @@ describe("accountService", () => { }); }); + describe("setAccountVerifyNewDeviceLogin", () => { + const initialState = true; + beforeEach(() => { + activeAccountIdState.stateSubject.next(userId); + singleUserStateProvider + .getFake(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN) + .nextState(initialState); + }); + + it("should update the VerifyNewDeviceLogin", async () => { + const expected = false; + expect(await firstValueFrom(sut.accountVerifyNewDeviceLogin$)).toEqual(initialState); + + await sut.setAccountVerifyNewDeviceLogin(userId, expected); + const currentState = await firstValueFrom(sut.accountVerifyNewDeviceLogin$); + + expect(currentState).toEqual(expected); + }); + + it("should NOT update VerifyNewDeviceLogin when userId is null", async () => { + await sut.setAccountVerifyNewDeviceLogin(null, false); + const currentState = await firstValueFrom(sut.accountVerifyNewDeviceLogin$); + + expect(currentState).toEqual(initialState); + }); + }); + describe("clean", () => { beforeEach(() => { accountsState.stateSubject.next({ [userId]: userInfo }); @@ -256,6 +312,7 @@ describe("accountService", () => { beforeEach(() => { accountsState.stateSubject.next({ [userId]: userInfo }); activeAccountIdState.stateSubject.next(userId); + accountActivityState.stateSubject.next({ [userId]: new Date(1) }); }); it("should emit null if no account is provided", async () => { @@ -269,6 +326,34 @@ describe("accountService", () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist"); }); + + it("should change active account when switched to the new account", async () => { + const newUserId = Utils.newGuid() as UserId; + accountsState.stateSubject.next({ [newUserId]: userInfo }); + + await sut.switchAccount(newUserId); + + await expect(firstValueFrom(sut.activeAccount$)).resolves.toEqual({ + id: newUserId, + ...userInfo, + }); + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({ + [userId]: new Date(1), + [newUserId]: expect.toAlmostEqual(new Date(), 1000), + }); + }); + + it("should not change active account when already switched to the same account", async () => { + await sut.switchAccount(userId); + + await expect(firstValueFrom(sut.activeAccount$)).resolves.toEqual({ + id: userId, + ...userInfo, + }); + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({ + [userId]: new Date(1), + }); + }); }); describe("account activity", () => { diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index d4479815c5d9..50ba2455d784 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -7,6 +7,10 @@ import { shareReplay, combineLatest, Observable, + switchMap, + filter, + timeout, + of, } from "rxjs"; import { @@ -23,6 +27,8 @@ import { GlobalState, GlobalStateProvider, KeyDefinition, + SingleUserStateProvider, + UserKeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; @@ -42,6 +48,15 @@ export const ACCOUNT_ACTIVITY = KeyDefinition.record(ACCOUNT_DISK, deserializer: (activity) => new Date(activity), }); +export const ACCOUNT_VERIFY_NEW_DEVICE_LOGIN = new UserKeyDefinition( + ACCOUNT_DISK, + "verifyNewDeviceLogin", + { + deserializer: (verifyDevices) => verifyDevices, + clearOn: ["logout"], + }, +); + const LOGGED_OUT_INFO: AccountInfo = { email: "", emailVerified: false, @@ -73,6 +88,7 @@ export class AccountServiceImplementation implements InternalAccountService { accounts$: Observable>; activeAccount$: Observable; accountActivity$: Observable>; + accountVerifyNewDeviceLogin$: Observable; sortedUserIds$: Observable; nextUpAccount$: Observable; @@ -80,6 +96,7 @@ export class AccountServiceImplementation implements InternalAccountService { private messagingService: MessagingService, private logService: LogService, private globalStateProvider: GlobalStateProvider, + private singleUserStateProvider: SingleUserStateProvider, ) { this.accountsState = this.globalStateProvider.get(ACCOUNT_ACCOUNTS); this.activeAccountIdState = this.globalStateProvider.get(ACCOUNT_ACTIVE_ACCOUNT_ID); @@ -114,6 +131,12 @@ export class AccountServiceImplementation implements InternalAccountService { return nextId ? { id: nextId, ...accounts[nextId] } : null; }), ); + this.accountVerifyNewDeviceLogin$ = this.activeAccountIdState.state$.pipe( + switchMap( + (userId) => + this.singleUserStateProvider.get(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).state$, + ), + ); } async addAccount(userId: UserId, accountData: AccountInfo): Promise { @@ -149,21 +172,28 @@ export class AccountServiceImplementation implements InternalAccountService { async switchAccount(userId: UserId | null): Promise { let updateActivity = false; await this.activeAccountIdState.update( - (_, accounts) => { - if (userId == null) { - // indicates no account is active - return null; - } - - if (accounts?.[userId] == null) { - throw new Error("Account does not exist"); - } + (_, __) => { updateActivity = true; return userId; }, { - combineLatestWith: this.accounts$, - shouldUpdate: (id) => { + combineLatestWith: this.accountsState.state$.pipe( + filter((accounts) => { + if (userId == null) { + // Don't worry about accounts when we are about to set active user to null + return true; + } + + return accounts?.[userId] != null; + }), + // If we don't get the desired account with enough time, just return empty as that will result in the same error + timeout({ first: 1000, with: () => of({} as Record) }), + ), + shouldUpdate: (id, accounts) => { + if (userId != null && accounts?.[userId] == null) { + throw new Error("Account does not exist"); + } + // update only if userId changes return id !== userId; }, @@ -193,6 +223,20 @@ export class AccountServiceImplementation implements InternalAccountService { ); } + async setAccountVerifyNewDeviceLogin( + userId: UserId, + setVerifyNewDeviceLogin: boolean, + ): Promise { + if (!Utils.isGuid(userId)) { + // only store for valid userIds + return; + } + + await this.singleUserStateProvider.get(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).update(() => { + return setVerifyNewDeviceLogin; + }); + } + async removeAccountActivity(userId: UserId): Promise { await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update( (activity) => { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 4b7b8a2b262d..fc236c91a21a 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -1,9 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { FakeAccountService, makeStaticByteArray, diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index d855a1be34f2..da70baf39990 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -11,9 +11,8 @@ import { switchMap, } from "rxjs"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { ApiService } from "../../abstractions/api.service"; import { StateService } from "../../platform/abstractions/state.service"; import { MessageSender } from "../../platform/messaging"; diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index a94c8b6e422e..fe43df53c48f 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -3,14 +3,12 @@ import { firstValueFrom, map, Observable } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { KeyService } from "@bitwarden/key-management"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; @@ -81,7 +79,17 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { private configService: ConfigService, ) { this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( - map((options) => options?.trustedDeviceOption != null ?? false), + map((options) => { + return options?.trustedDeviceOption != null ?? false; + }), + ); + } + + supportsDeviceTrustByUserId$(userId: UserId): Observable { + return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe( + map((options) => { + return options?.trustedDeviceOption != null ?? false; + }), ); } diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 943653e3129f..e689c93395d7 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -1,22 +1,22 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { matches, mock } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { KeyService } from "@bitwarden/key-management"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { DeviceType } from "../../enums"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; @@ -74,17 +74,56 @@ describe("deviceTrustService", () => { userId: mockUserId, }; + let userDecryptionOptions: UserDecryptionOptions; + beforeEach(() => { jest.clearAllMocks(); const supportsSecureStorage = false; // default to false; tests will override as needed // By default all the tests will have a mocked active user in state provider. deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); + + userDecryptionOptions = new UserDecryptionOptions(); }); it("instantiates", () => { expect(deviceTrustService).not.toBeFalsy(); }); + describe("supportsDeviceTrustByUserId$", () => { + it("returns true when the user has a non-null trusted device decryption option", async () => { + // Arrange + userDecryptionOptions.trustedDeviceOption = { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: false, + isTdeOffboarding: false, + }; + + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + new BehaviorSubject(userDecryptionOptions), + ); + + const result = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(mockUserId), + ); + expect(result).toBe(true); + }); + + it("returns false when the user has a null trusted device decryption option", async () => { + // Arrange + userDecryptionOptions.trustedDeviceOption = null; + + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + new BehaviorSubject(userDecryptionOptions), + ); + + const result = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(mockUserId), + ); + expect(result).toBe(false); + }); + }); + describe("User Trust Device Choice For Decryption", () => { describe("getShouldTrustDevice", () => { it("gets the user trust device choice for decryption", async () => { diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index 660f1124f4a5..ec03c7ece557 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -1,11 +1,11 @@ import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { KeyService } from "@bitwarden/key-management"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; import { ApiService } from "../../abstractions/api.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../admin-console/models/data/organization.data"; import { Organization } from "../../admin-console/models/domain/organization"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; @@ -95,7 +95,7 @@ describe("KeyConnectorService", () => { organizationData(true, false, "https://key-connector-url.com", 2, false), organizationData(true, true, "https://other-url.com", 2, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -110,7 +110,7 @@ describe("KeyConnectorService", () => { organizationData(true, false, "https://key-connector-url.com", 2, false), organizationData(false, false, "https://key-connector-url.com", 2, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -125,7 +125,7 @@ describe("KeyConnectorService", () => { organizationData(true, true, "https://key-connector-url.com", 0, false), organizationData(true, true, "https://key-connector-url.com", 1, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -140,7 +140,7 @@ describe("KeyConnectorService", () => { organizationData(true, true, "https://key-connector-url.com", 2, true), organizationData(false, true, "https://key-connector-url.com", 2, true), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -181,7 +181,7 @@ describe("KeyConnectorService", () => { // create organization object const data = organizationData(true, true, "https://key-connector-url.com", 2, false); - organizationService.getAll.mockResolvedValue([data]); + organizationService.organizations$.mockReturnValue(of([data])); // uses KeyConnector const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); @@ -195,7 +195,7 @@ describe("KeyConnectorService", () => { it("should return false if the user does not need migration", async () => { tokenService.getIsExternal.mockResolvedValue(false); const data = organizationData(false, false, "https://key-connector-url.com", 2, false); - organizationService.getAll.mockResolvedValue([data]); + organizationService.organizations$.mockReturnValue(of([data])); const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); state.nextState(true); @@ -275,7 +275,7 @@ describe("KeyConnectorService", () => { const masterKey = getMockMasterKey(); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); const error = new Error("Failed to post user key to key connector"); - organizationService.getAll.mockResolvedValue([organization]); + organizationService.organizations$.mockReturnValue(of([organization])); masterPasswordService.masterKeySubject.next(masterKey); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); @@ -366,6 +366,7 @@ describe("KeyConnectorService", () => { accessSecretsManager: false, limitCollectionCreation: true, limitCollectionDeletion: true, + limitItemDeletion: true, allowAdminAccessToAllCollectionItems: true, flexibleCollections: false, object: "profileOrganization", diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f798413162ee..f6f76579ee51 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -3,6 +3,8 @@ import { firstValueFrom } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Argon2KdfConfig, KdfConfig, @@ -12,7 +14,6 @@ import { } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; import { Organization } from "../../admin-console/models/domain/organization"; import { KeysRequest } from "../../models/request/keys.request"; @@ -28,7 +29,6 @@ import { } from "../../platform/state"; import { UserId } from "../../types/guid"; import { MasterKey } from "../../types/key"; -import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; @@ -122,7 +122,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async getManagingOrganization(userId?: UserId): Promise { - const orgs = await this.organizationService.getAll(userId); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); return orgs.find( (o) => o.keyConnectorEnabled && diff --git a/libs/common/src/auth/services/master-password/master-password.service.ts b/libs/common/src/auth/services/master-password/master-password.service.ts index 14e7522a836c..9b5ce588bd32 100644 --- a/libs/common/src/auth/services/master-password/master-password.service.ts +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable } from "rxjs"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { StateService } from "../../../platform/abstractions/state.service"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index ddd24ae7907e..8516400fe095 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -2,13 +2,11 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { KeyService } from "@bitwarden/key-management"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { UserId } from "../../types/guid"; import { Account, AccountInfo, AccountService } from "../abstractions/account.service"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index 22d5384e6ac0..824521d8a2e9 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -6,12 +6,10 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, } from "@bitwarden/admin-console/common"; +import { KeyService } from "@bitwarden/key-management"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { Utils } from "../../platform/misc/utils"; import { UserKey } from "../../types/key"; diff --git a/libs/common/src/auth/services/sso-login.service.spec.ts b/libs/common/src/auth/services/sso-login.service.spec.ts new file mode 100644 index 000000000000..6764755e6ca3 --- /dev/null +++ b/libs/common/src/auth/services/sso-login.service.spec.ts @@ -0,0 +1,94 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { + CODE_VERIFIER, + GLOBAL_ORGANIZATION_SSO_IDENTIFIER, + SSO_EMAIL, + SSO_STATE, + SsoLoginService, + USER_ORGANIZATION_SSO_IDENTIFIER, +} from "@bitwarden/common/auth/services/sso-login.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; + +describe("SSOLoginService ", () => { + let sut: SsoLoginService; + + let accountService: FakeAccountService; + let mockSingleUserStateProvider: FakeStateProvider; + let mockLogService: MockProxy; + let userId: UserId; + + beforeEach(() => { + jest.clearAllMocks(); + + userId = Utils.newGuid() as UserId; + accountService = mockAccountServiceWith(userId); + mockSingleUserStateProvider = new FakeStateProvider(accountService); + mockLogService = mock(); + + sut = new SsoLoginService(mockSingleUserStateProvider, mockLogService); + }); + + it("instantiates", () => { + expect(sut).not.toBeFalsy(); + }); + + it("gets and sets code verifier", async () => { + const codeVerifier = "test-code-verifier"; + await sut.setCodeVerifier(codeVerifier); + mockSingleUserStateProvider.getGlobal(CODE_VERIFIER); + + const result = await sut.getCodeVerifier(); + expect(result).toBe(codeVerifier); + }); + + it("gets and sets SSO state", async () => { + const ssoState = "test-sso-state"; + await sut.setSsoState(ssoState); + mockSingleUserStateProvider.getGlobal(SSO_STATE); + + const result = await sut.getSsoState(); + expect(result).toBe(ssoState); + }); + + it("gets and sets organization SSO identifier", async () => { + const orgIdentifier = "test-org-identifier"; + await sut.setOrganizationSsoIdentifier(orgIdentifier); + mockSingleUserStateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER); + + const result = await sut.getOrganizationSsoIdentifier(); + expect(result).toBe(orgIdentifier); + }); + + it("gets and sets SSO email", async () => { + const email = "test@example.com"; + await sut.setSsoEmail(email); + mockSingleUserStateProvider.getGlobal(SSO_EMAIL); + + const result = await sut.getSsoEmail(); + expect(result).toBe(email); + }); + + it("gets and sets active user organization SSO identifier", async () => { + const userId = Utils.newGuid() as UserId; + const orgIdentifier = "test-active-org-identifier"; + await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, userId); + mockSingleUserStateProvider.getUser(userId, USER_ORGANIZATION_SSO_IDENTIFIER); + + const result = await sut.getActiveUserOrganizationSsoIdentifier(userId); + expect(result).toBe(orgIdentifier); + }); + + it("logs error when setting active user organization SSO identifier with undefined userId", async () => { + const orgIdentifier = "test-active-org-identifier"; + await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, undefined); + + expect(mockLogService.error).toHaveBeenCalledWith( + "Tried to set a user organization sso identifier with an undefined user id.", + ); + }); +}); diff --git a/libs/common/src/auth/services/sso-login.service.ts b/libs/common/src/auth/services/sso-login.service.ts index 32019e8d568b..9b4b86567825 100644 --- a/libs/common/src/auth/services/sso-login.service.ts +++ b/libs/common/src/auth/services/sso-login.service.ts @@ -1,11 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { firstValueFrom } from "rxjs"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; + import { - ActiveUserState, GlobalState, KeyDefinition, + SingleUserState, SSO_DISK, StateProvider, UserKeyDefinition, @@ -15,21 +16,21 @@ import { SsoLoginServiceAbstraction } from "../abstractions/sso-login.service.ab /** * Uses disk storage so that the code verifier can be persisted across sso redirects. */ -const CODE_VERIFIER = new KeyDefinition(SSO_DISK, "ssoCodeVerifier", { +export const CODE_VERIFIER = new KeyDefinition(SSO_DISK, "ssoCodeVerifier", { deserializer: (codeVerifier) => codeVerifier, }); /** * Uses disk storage so that the sso state can be persisted across sso redirects. */ -const SSO_STATE = new KeyDefinition(SSO_DISK, "ssoState", { +export const SSO_STATE = new KeyDefinition(SSO_DISK, "ssoState", { deserializer: (state) => state, }); /** * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. */ -const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition( +export const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition( SSO_DISK, "organizationSsoIdentifier", { @@ -41,7 +42,7 @@ const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition( /** * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. */ -const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( +export const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( SSO_DISK, "organizationSsoIdentifier", { @@ -52,7 +53,7 @@ const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( /** * Uses disk storage so that the user's email can be persisted across sso redirects. */ -const SSO_EMAIL = new KeyDefinition(SSO_DISK, "ssoEmail", { +export const SSO_EMAIL = new KeyDefinition(SSO_DISK, "ssoEmail", { deserializer: (state) => state, }); @@ -61,19 +62,18 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { private ssoState: GlobalState; private orgSsoIdentifierState: GlobalState; private ssoEmailState: GlobalState; - private activeUserOrgSsoIdentifierState: ActiveUserState; - constructor(private stateProvider: StateProvider) { + constructor( + private stateProvider: StateProvider, + private logService: LogService, + ) { this.codeVerifierState = this.stateProvider.getGlobal(CODE_VERIFIER); this.ssoState = this.stateProvider.getGlobal(SSO_STATE); this.orgSsoIdentifierState = this.stateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER); this.ssoEmailState = this.stateProvider.getGlobal(SSO_EMAIL); - this.activeUserOrgSsoIdentifierState = this.stateProvider.getActive( - USER_ORGANIZATION_SSO_IDENTIFIER, - ); } - getCodeVerifier(): Promise { + getCodeVerifier(): Promise { return firstValueFrom(this.codeVerifierState.state$); } @@ -81,7 +81,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { await this.codeVerifierState.update((_) => codeVerifier); } - getSsoState(): Promise { + getSsoState(): Promise { return firstValueFrom(this.ssoState.state$); } @@ -89,7 +89,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { await this.ssoState.update((_) => ssoState); } - getOrganizationSsoIdentifier(): Promise { + getOrganizationSsoIdentifier(): Promise { return firstValueFrom(this.orgSsoIdentifierState.state$); } @@ -97,7 +97,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { await this.orgSsoIdentifierState.update((_) => organizationIdentifier); } - getSsoEmail(): Promise { + getSsoEmail(): Promise { return firstValueFrom(this.ssoEmailState.state$); } @@ -105,11 +105,24 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { await this.ssoEmailState.update((_) => email); } - getActiveUserOrganizationSsoIdentifier(): Promise { - return firstValueFrom(this.activeUserOrgSsoIdentifierState.state$); + getActiveUserOrganizationSsoIdentifier(userId: UserId): Promise { + return firstValueFrom(this.userOrgSsoIdentifierState(userId).state$); + } + + async setActiveUserOrganizationSsoIdentifier( + organizationIdentifier: string, + userId: UserId | undefined, + ): Promise { + if (userId === undefined) { + this.logService.error( + "Tried to set a user organization sso identifier with an undefined user id.", + ); + return; + } + await this.userOrgSsoIdentifierState(userId).update((_) => organizationIdentifier); } - async setActiveUserOrganizationSsoIdentifier(organizationIdentifier: string): Promise { - await this.activeUserOrgSsoIdentifierState.update((_) => organizationIdentifier); + private userOrgSsoIdentifierState(userId: UserId): SingleUserState { + return this.stateProvider.getUser(userId, USER_ORGANIZATION_SSO_IDENTIFIER); } } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index f8882e1b1180..339f570a003f 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -5,7 +5,7 @@ import { LogoutReason } from "@bitwarden/auth/common"; import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 4b7cc2cab01c..72e082f20025 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -6,7 +6,7 @@ import { Opaque } from "type-fest"; import { LogoutReason, decodeJwtTokenToJson } from "@bitwarden/auth/common"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts index 102e4bac8da1..677b6ff44999 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -12,11 +12,9 @@ import { BiometricsStatus, KdfConfig, KeyService, + KdfConfigService, } from "@bitwarden/key-management"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KdfConfigService } from "../../../../../key-management/src/abstractions/kdf-config.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 1e68488ac987..69309014fac0 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -4,7 +4,6 @@ import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; -import { BillingSourceResponse } from "../models/response/billing.response"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; export type OrganizationInformation = { @@ -46,9 +45,7 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - getPaymentSource: ( - organizationId: string, - ) => Promise; + getPaymentSource: (organizationId: string) => Promise; purchaseSubscription: (subscription: SubscriptionInformation) => Promise; diff --git a/libs/common/src/billing/models/response/tax-rate.response.ts b/libs/common/src/billing/models/response/tax-rate.response.ts deleted file mode 100644 index 2c07129ba2c3..000000000000 --- a/libs/common/src/billing/models/response/tax-rate.response.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseResponse } from "../../../models/response/base.response"; - -export class TaxRateResponse extends BaseResponse { - id: string; - country: string; - state: string; - postalCode: string; - rate: number; - - constructor(response: any) { - super(response); - this.id = this.getResponseProperty("Id"); - this.country = this.getResponseProperty("Country"); - this.state = this.getResponseProperty("State"); - this.postalCode = this.getResponseProperty("PostalCode"); - this.rate = this.getResponseProperty("Rate"); - } -} diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index da1a1666ff0f..83efbf0a30c0 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -7,9 +7,7 @@ import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../ import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; -import { FeatureFlag } from "../../enums/feature-flag.enum"; -import { ConfigService } from "../../platform/abstractions/config/config.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { SyncService } from "../../platform/sync"; @@ -24,7 +22,6 @@ import { } from "../abstractions"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; -import { BillingSourceResponse } from "../models/response/billing.response"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; interface OrganizationKeys { @@ -38,7 +35,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs constructor( private apiService: ApiService, private billingApiService: BillingApiServiceAbstraction, - private configService: ConfigService, private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, @@ -46,21 +42,9 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} - async getPaymentSource( - organizationId: string, - ): Promise { - const deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - - if (deprecateStripeSourcesAPI) { - const paymentMethod = - await this.billingApiService.getOrganizationPaymentMethod(organizationId); - return paymentMethod.paymentSource; - } else { - const billing = await this.organizationApiService.getBilling(organizationId); - return billing.paymentSource; - } + async getPaymentSource(organizationId: string): Promise { + const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId); + return paymentMethod.paymentSource; } async purchaseSubscription(subscription: SubscriptionInformation): Promise { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d008a09d66cb..a8e036c82d67 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -4,11 +4,19 @@ * Flags MUST be short lived and SHALL be removed once enabled. */ export enum FeatureFlag { + /* Admin Console Team */ + ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", + AccountDeprovisioning = "pm-10308-account-deprovisioning", + VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", + PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page", + LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission", + /* Autofill */ BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", + IdpAutoSubmitLogin = "idp-auto-submit-login", InlineMenuFieldQualification = "inline-menu-field-qualification", InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", InlineMenuTotp = "inline-menu-totp", @@ -16,37 +24,33 @@ export enum FeatureFlag { NotificationRefresh = "notification-refresh", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", - BrowserFilelessImport = "browser-fileless-import", + /* Tools */ ItemShare = "item-share", - GeneratorToolsModernization = "generator-tools-modernization", + CriticalApps = "pm-14466-risk-insights-critical-application", + EnableRiskInsightsNotifications = "enable-risk-insights-notifications", + AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", ExtensionRefresh = "extension-refresh", PersistPopupView = "persist-popup-view", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", - EmailVerification = "email-verification", TwoFactorComponentRefactor = "two-factor-component-refactor", - ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", - IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", - AccountDeprovisioning = "pm-10308-account-deprovisioning", SSHKeyVaultItem = "ssh-key-vault-item", SSHAgent = "ssh-agent", - AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", - VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", - PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page", - CriticalApps = "pm-14466-risk-insights-critical-application", TrialPaymentOptional = "PM-8163-trial-payment", SecurityTasks = "security-tasks", NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", - DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", MacOsNativeCredentialSync = "macos-native-credential-sync", - PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", + PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", PrivateKeyRegeneration = "pm-12241-private-key-regeneration", ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs", + AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner", + NewDeviceVerification = "new-device-verification", + PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -61,11 +65,19 @@ const FALSE = false as boolean; * We support true as a value as we prefer flags to "enable" not "disable". */ export const DefaultFeatureFlagValue = { + /* Admin Console Team */ + [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, + [FeatureFlag.AccountDeprovisioning]: FALSE, + [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, + [FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE, + [FeatureFlag.LimitItemDeletion]: FALSE, + /* Autofill */ [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, + [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.InlineMenuFieldQualification]: FALSE, [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.InlineMenuTotp]: FALSE, @@ -73,37 +85,33 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.NotificationRefresh]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, - [FeatureFlag.BrowserFilelessImport]: FALSE, + /* Tools */ [FeatureFlag.ItemShare]: FALSE, - [FeatureFlag.GeneratorToolsModernization]: FALSE, + [FeatureFlag.CriticalApps]: FALSE, + [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, + [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.PersistPopupView]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, - [FeatureFlag.EmailVerification]: FALSE, [FeatureFlag.TwoFactorComponentRefactor]: FALSE, - [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, - [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, - [FeatureFlag.AccountDeprovisioning]: FALSE, [FeatureFlag.SSHKeyVaultItem]: FALSE, [FeatureFlag.SSHAgent]: FALSE, - [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, - [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, - [FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE, - [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, - [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, - [FeatureFlag.PM12443RemovePagingLogic]: FALSE, + [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.ResellerManagedOrgAlert]: FALSE, + [FeatureFlag.AccountDeprovisioningBanner]: FALSE, + [FeatureFlag.NewDeviceVerification]: FALSE, + [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/enums/push-technology.enum.ts b/libs/common/src/enums/push-technology.enum.ts new file mode 100644 index 000000000000..9452c144bb7e --- /dev/null +++ b/libs/common/src/enums/push-technology.enum.ts @@ -0,0 +1,13 @@ +/** + * The preferred push technology of the server. + */ +export enum PushTechnology { + /** + * Indicates that we should use SignalR over web sockets to receive push notifications from the server. + */ + SignalR = 0, + /** + * Indicatates that we should use WebPush to receive push notifications from the server. + */ + WebPush = 1, +} diff --git a/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts new file mode 100644 index 000000000000..3e47ccdb5f25 --- /dev/null +++ b/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts @@ -0,0 +1,10 @@ +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +export abstract class BulkEncryptService { + abstract decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise; +} diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts similarity index 82% rename from libs/common/src/platform/abstractions/encrypt.service.ts rename to libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index a660524699d0..e00d053ce7b5 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -1,9 +1,9 @@ -import { Decryptable } from "../interfaces/decryptable.interface"; -import { Encrypted } from "../interfaces/encrypted"; -import { InitializerMetadata } from "../interfaces/initializer-metadata.interface"; -import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; -import { EncString } from "../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; diff --git a/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts similarity index 84% rename from libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts rename to libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts index 1320fbae0e08..1d1e0f522790 100644 --- a/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts @@ -3,15 +3,14 @@ import { firstValueFrom, fromEvent, filter, map, takeUntil, defaultIfEmpty, Subject } from "rxjs"; import { Jsonify } from "type-fest"; -import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; -import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; -import { LogService } from "../../abstractions/log.service"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; -import { Utils } from "../../misc/utils"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; - -import { getClassInitializer } from "./get-class-initializer"; +import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer"; // TTL (time to live) is not strictly required but avoids tying up memory resources if inactive const workerTTL = 60000; // 1 minute @@ -88,7 +87,7 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService { new Worker( new URL( /* webpackChunkName: 'encrypt-worker' */ - "@bitwarden/common/platform/services/cryptography/encrypt.worker.ts", + "@bitwarden/common/key-management/crypto/services/encrypt.worker.ts", import.meta.url, ), ), diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts similarity index 90% rename from libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts rename to libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index 68263cadf271..075b9da49640 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -1,17 +1,21 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Utils } from "../../../platform/misc/utils"; -import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { LogService } from "../../abstractions/log.service"; -import { EncryptionType, encryptionTypeToString as encryptionTypeName } from "../../enums"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { Encrypted } from "../../interfaces/encrypted"; -import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; -import { EncArrayBuffer } from "../../models/domain/enc-array-buffer"; -import { EncString } from "../../models/domain/enc-string"; -import { EncryptedObject } from "../../models/domain/encrypted-object"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + EncryptionType, + encryptionTypeToString as encryptionTypeName, +} from "@bitwarden/common/platform/enums"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +import { EncryptService } from "../abstractions/encrypt.service"; export class EncryptServiceImplementation implements EncryptService { constructor( diff --git a/libs/common/src/platform/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts similarity index 91% rename from libs/common/src/platform/services/encrypt.service.spec.ts rename to libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 609b5100a10a..8d75b5285966 100644 --- a/libs/common/src/platform/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -1,15 +1,17 @@ import { mockReset, mock } from "jest-mock-extended"; -import { makeStaticByteArray } from "../../../spec"; -import { CsprngArray } from "../../types/csprng"; -import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { LogService } from "../abstractions/log.service"; -import { EncryptionType } from "../enums"; -import { Utils } from "../misc/utils"; -import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; -import { EncString } from "../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; -import { EncryptServiceImplementation } from "../services/cryptography/encrypt.service.implementation"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +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 { makeStaticByteArray } from "../../../../spec"; + +import { EncryptServiceImplementation } from "./encrypt.service.implementation"; describe("EncryptService", () => { const cryptoFunctionService = mock(); diff --git a/libs/common/src/platform/services/cryptography/encrypt.worker.ts b/libs/common/src/key-management/crypto/services/encrypt.worker.ts similarity index 71% rename from libs/common/src/platform/services/cryptography/encrypt.worker.ts rename to libs/common/src/key-management/crypto/services/encrypt.worker.ts index a293e1c6bb02..84ffcf569343 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.worker.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.worker.ts @@ -2,14 +2,14 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { ConsoleLogService } from "../console-log.service"; -import { ContainerService } from "../container.service"; -import { WebCryptoFunctionService } from "../web-crypto-function.service"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer"; +import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { EncryptServiceImplementation } from "./encrypt.service.implementation"; -import { getClassInitializer } from "./get-class-initializer"; const workerApi: Worker = self as any; diff --git a/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts similarity index 68% rename from libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts rename to libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts index 7a4fd8f3c1db..80fdd27895d4 100644 --- a/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts +++ b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts @@ -1,10 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; +import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +import { EncryptService } from "../abstractions/encrypt.service"; /** * @deprecated For the feature flag from PM-4154, remove once feature is rolled out diff --git a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts similarity index 81% rename from libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts rename to libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts index 100dcf152e65..0bf968515632 100644 --- a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts @@ -3,13 +3,13 @@ import { defaultIfEmpty, filter, firstValueFrom, fromEvent, map, Subject, takeUntil } from "rxjs"; import { Jsonify } from "type-fest"; -import { Utils } from "../../../platform/misc/utils"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer"; import { EncryptServiceImplementation } from "./encrypt.service.implementation"; -import { getClassInitializer } from "./get-class-initializer"; // TTL (time to live) is not strictly required but avoids tying up memory resources if inactive const workerTTL = 3 * 60000; // 3 minutes @@ -40,7 +40,7 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple this.worker ??= new Worker( new URL( /* webpackChunkName: 'encrypt-worker' */ - "@bitwarden/common/platform/services/cryptography/encrypt.worker.ts", + "@bitwarden/common/key-management/crypto/services/encrypt.worker.ts", import.meta.url, ), ); diff --git a/libs/common/src/key-management/services/default-process-reload.service.ts b/libs/common/src/key-management/services/default-process-reload.service.ts index 9f97e0a94c1b..09fe4e7e10c7 100644 --- a/libs/common/src/key-management/services/default-process-reload.service.ts +++ b/libs/common/src/key-management/services/default-process-reload.service.ts @@ -2,11 +2,9 @@ // @ts-strict-ignore import { firstValueFrom, map, timeout } from "rxjs"; +import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { BiometricStateService } from "@bitwarden/key-management"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; diff --git a/libs/common/src/models/response/profile.response.ts b/libs/common/src/models/response/profile.response.ts index 6b6555fc5669..9aee5acbce8c 100644 --- a/libs/common/src/models/response/profile.response.ts +++ b/libs/common/src/models/response/profile.response.ts @@ -21,6 +21,7 @@ export class ProfileResponse extends BaseResponse { securityStamp: string; forcePasswordReset: boolean; usesKeyConnector: boolean; + verifyDevices: boolean; organizations: ProfileOrganizationResponse[] = []; providers: ProfileProviderResponse[] = []; providerOrganizations: ProfileProviderOrganizationResponse[] = []; @@ -42,6 +43,7 @@ export class ProfileResponse extends BaseResponse { this.securityStamp = this.getResponseProperty("SecurityStamp"); this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false; this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false; + this.verifyDevices = this.getResponseProperty("VerifyDevices") ?? true; const organizations = this.getResponseProperty("Organizations"); if (organizations != null) { diff --git a/libs/common/src/platform/abstractions/bulk-encrypt.service.ts b/libs/common/src/platform/abstractions/bulk-encrypt.service.ts deleted file mode 100644 index 4cdff0c769ae..000000000000 --- a/libs/common/src/platform/abstractions/bulk-encrypt.service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Decryptable } from "../interfaces/decryptable.interface"; -import { InitializerMetadata } from "../interfaces/initializer-metadata.interface"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; - -export abstract class BulkEncryptService { - abstract decryptItems( - items: Decryptable[], - key: SymmetricCryptoKey, - ): Promise; -} diff --git a/libs/common/src/platform/abstractions/config/server-config.ts b/libs/common/src/platform/abstractions/config/server-config.ts index f77239b3016b..8e08cc4e16cc 100644 --- a/libs/common/src/platform/abstractions/config/server-config.ts +++ b/libs/common/src/platform/abstractions/config/server-config.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; +import { PushTechnology } from "../../../enums/push-technology.enum"; import { ServerConfigData, ThirdPartyServerConfigData, @@ -10,6 +11,11 @@ import { } from "../../models/data/server-config.data"; import { ServerSettings } from "../../models/domain/server-settings"; +type PushConfig = + | { pushTechnology: PushTechnology.SignalR } + | { pushTechnology: PushTechnology.WebPush; vapidPublicKey: string } + | undefined; + const dayInMilliseconds = 24 * 3600 * 1000; export class ServerConfig { @@ -19,6 +25,7 @@ export class ServerConfig { environment?: EnvironmentServerConfigData; utcDate: Date; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; + push: PushConfig; settings: ServerSettings; constructor(serverConfigData: ServerConfigData) { @@ -28,6 +35,15 @@ export class ServerConfig { this.utcDate = new Date(serverConfigData.utcDate); this.environment = serverConfigData.environment; this.featureStates = serverConfigData.featureStates; + this.push = + serverConfigData.push == null + ? { + pushTechnology: PushTechnology.SignalR, + } + : { + pushTechnology: serverConfigData.push.pushTechnology, + vapidPublicKey: serverConfigData.push.vapidPublicKey, + }; this.settings = serverConfigData.settings; if (this.server?.name == null && this.server?.url == null) { diff --git a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts index d684561dacdc..6a1b7b67b428 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts @@ -4,6 +4,10 @@ import type { BitwardenClient } from "@bitwarden/sdk-internal"; * Factory for creating SDK clients. */ export abstract class SdkClientFactory { + /** + * Creates a new BitwardenClient. Assumes the SDK is already loaded. + * @param args Bitwarden client constructor parameters + */ abstract createSdkClient( ...args: ConstructorParameters ): Promise; diff --git a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts new file mode 100644 index 000000000000..16482e797b26 --- /dev/null +++ b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts @@ -0,0 +1,3 @@ +export abstract class SdkLoadService { + abstract load(): Promise; +} diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index d44d38c36ab9..22ad2b44ff92 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -1,22 +1,21 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { BitwardenClient } from "@bitwarden/sdk-internal"; import { UserId } from "../../../types/guid"; +import { Rc } from "../../misc/reference-counting/rc"; export abstract class SdkService { /** * Retrieve the version of the SDK. */ - version$: Observable; + abstract version$: Observable; /** * Retrieve a client initialized without a user. * This client can only be used for operations that don't require a user context. */ - client$: Observable; + abstract client$: Observable; /** * Retrieve a client initialized for a specific user. @@ -29,5 +28,5 @@ export abstract class SdkService { * * @param userId */ - abstract userClient$(userId: UserId): Observable; + abstract userClient$(userId: UserId): Observable | undefined>; } diff --git a/libs/common/src/platform/misc/reference-counting/rc.spec.ts b/libs/common/src/platform/misc/reference-counting/rc.spec.ts new file mode 100644 index 000000000000..094abfe36156 --- /dev/null +++ b/libs/common/src/platform/misc/reference-counting/rc.spec.ts @@ -0,0 +1,93 @@ +// Temporary workaround for Symbol.dispose +// remove when https://github.com/jestjs/jest/issues/14874 is resolved and *released* +const disposeSymbol: unique symbol = Symbol("Symbol.dispose"); +const asyncDisposeSymbol: unique symbol = Symbol("Symbol.asyncDispose"); +(Symbol as any).asyncDispose ??= asyncDisposeSymbol as unknown as SymbolConstructor["asyncDispose"]; +(Symbol as any).dispose ??= disposeSymbol as unknown as SymbolConstructor["dispose"]; + +// Import needs to be after the workaround +import { Rc } from "./rc"; + +export class FreeableTestValue { + isFreed = false; + + free() { + this.isFreed = true; + } +} + +describe("Rc", () => { + let value: FreeableTestValue; + let rc: Rc; + + beforeEach(() => { + value = new FreeableTestValue(); + rc = new Rc(value); + }); + + it("should increase refCount when taken", () => { + rc.take(); + + expect(rc["refCount"]).toBe(1); + }); + + it("should return value on take", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + + expect(reference.value).toBe(value); + }); + + it("should decrease refCount when disposing reference", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + + reference[Symbol.dispose](); + + expect(rc["refCount"]).toBe(0); + }); + + it("should automatically decrease refCount when reference goes out of scope", () => { + { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + using reference = rc.take(); + } + + expect(rc["refCount"]).toBe(0); + }); + + it("should not free value when refCount reaches 0 if not marked for disposal", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + + reference[Symbol.dispose](); + + expect(value.isFreed).toBe(false); + }); + + it("should free value when refCount reaches 0 and rc is marked for disposal", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + rc.markForDisposal(); + + reference[Symbol.dispose](); + + expect(value.isFreed).toBe(true); + }); + + it("should free value when marked for disposal if refCount is 0", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + reference[Symbol.dispose](); + + rc.markForDisposal(); + + expect(value.isFreed).toBe(true); + }); + + it("should throw error when trying to take a disposed reference", () => { + rc.markForDisposal(); + + expect(() => rc.take()).toThrow(); + }); +}); diff --git a/libs/common/src/platform/misc/reference-counting/rc.ts b/libs/common/src/platform/misc/reference-counting/rc.ts new file mode 100644 index 000000000000..9be102b43d3d --- /dev/null +++ b/libs/common/src/platform/misc/reference-counting/rc.ts @@ -0,0 +1,76 @@ +import { UsingRequired } from "../using-required"; + +export type Freeable = { free: () => void }; + +/** + * Reference counted disposable value. + * This class is used to manage the lifetime of a value that needs to be + * freed of at a specific time but might still be in-use when that happens. + */ +export class Rc { + private markedForDisposal = false; + private refCount = 0; + private value: T; + + constructor(value: T) { + this.value = value; + } + + /** + * Use this function when you want to use the underlying object. + * This will guarantee that you have a reference to the object + * and that it won't be freed until your reference goes out of scope. + * + * This function must be used with the `using` keyword. + * + * @example + * ```typescript + * function someFunction(rc: Rc) { + * using reference = rc.take(); + * reference.value.doSomething(); + * // reference is automatically disposed here + * } + * ``` + * + * @returns The value. + */ + take(): Ref { + if (this.markedForDisposal) { + throw new Error("Cannot take a reference to a value marked for disposal"); + } + + this.refCount++; + return new Ref(() => this.release(), this.value); + } + + /** + * Mark this Rc for disposal. When the refCount reaches 0, the value + * will be freed. + */ + markForDisposal() { + this.markedForDisposal = true; + this.freeIfPossible(); + } + + private release() { + this.refCount--; + this.freeIfPossible(); + } + + private freeIfPossible() { + if (this.refCount === 0 && this.markedForDisposal) { + this.value.free(); + } + } +} + +export class Ref implements UsingRequired { + constructor( + private readonly release: () => void, + readonly value: T, + ) {} + + [Symbol.dispose]() { + this.release(); + } +} diff --git a/libs/common/src/platform/misc/support-status.ts b/libs/common/src/platform/misc/support-status.ts new file mode 100644 index 000000000000..6e02a10c8d8b --- /dev/null +++ b/libs/common/src/platform/misc/support-status.ts @@ -0,0 +1,48 @@ +import { ObservableInput, OperatorFunction, switchMap } from "rxjs"; + +/** + * Indicates that the given set of actions is not supported and there is + * not anything the user can do to make it supported. The reason property + * should contain a documented and machine readable string so more in + * depth details can be shown to the user. + */ +export type NotSupported = { type: "not-supported"; reason: string }; + +/** + * Indicates that the given set of actions does not currently work but + * could be supported if configuration, either inside Bitwarden or outside, + * is done. The reason property should contain a documented and + * machine readable string so further instruction can be supplied to the caller. + */ +export type NeedsConfiguration = { type: "needs-configuration"; reason: string }; + +/** + * Indicates that the actions in the service property are supported. + */ +export type Supported = { type: "supported"; service: T }; + +/** + * A type encapsulating the status of support for a service. + */ +export type SupportStatus = Supported | NeedsConfiguration | NotSupported; + +/** + * Projects each source value to one of the given projects defined in `selectors`. + * + * @param selectors.supported The function to run when the given item reports that it is supported + * @param selectors.notSupported The function to run when the given item reports that it is either not-supported + * or needs-configuration. + * @returns A function that returns an Observable that emits the result of one of the given projection functions. + */ +export function supportSwitch(selectors: { + supported: (service: TService, index: number) => ObservableInput; + notSupported: (reason: string, index: number) => ObservableInput; +}): OperatorFunction, TSupported | TNotSupported> { + return switchMap((supportStatus, index) => { + if (supportStatus.type === "supported") { + return selectors.supported(supportStatus.service, index); + } + + return selectors.notSupported(supportStatus.reason, index); + }); +} diff --git a/libs/common/src/platform/misc/using-required.ts b/libs/common/src/platform/misc/using-required.ts new file mode 100644 index 000000000000..f641b9f312fa --- /dev/null +++ b/libs/common/src/platform/misc/using-required.ts @@ -0,0 +1,11 @@ +export type Disposable = { [Symbol.dispose]: () => void }; + +/** + * Types implementing this type must be used together with the `using` keyword + * + * @example using ref = rc.take(); + */ +// We want to use `interface` here because it creates a separate type. +// Type aliasing would not expose `UsingRequired` to the linter. +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UsingRequired extends Disposable {} diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index f654897e9e2e..ef65d2130a0a 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -8,10 +8,9 @@ import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; -import { EncryptService } from "../abstractions/encrypt.service"; +import { KeyService } from "@bitwarden/key-management"; + +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../abstractions/i18n.service"; // FIXME: Remove when updating file. Eslint update diff --git a/libs/common/src/platform/models/data/server-config.data.spec.ts b/libs/common/src/platform/models/data/server-config.data.spec.ts index 13d14204085c..d71e76657fd6 100644 --- a/libs/common/src/platform/models/data/server-config.data.spec.ts +++ b/libs/common/src/platform/models/data/server-config.data.spec.ts @@ -1,3 +1,4 @@ +import { PushTechnology } from "../../../enums/push-technology.enum"; import { Region } from "../../abstractions/environment.service"; import { @@ -29,6 +30,9 @@ describe("ServerConfigData", () => { }, utcDate: "2020-01-01T00:00:00.000Z", featureStates: { feature: "state" }, + push: { + pushTechnology: PushTechnology.SignalR, + }, }; const serverConfigData = ServerConfigData.fromJSON(json); diff --git a/libs/common/src/platform/models/data/server-config.data.ts b/libs/common/src/platform/models/data/server-config.data.ts index 6ed51d2f5ce5..af99f1d4a6d9 100644 --- a/libs/common/src/platform/models/data/server-config.data.ts +++ b/libs/common/src/platform/models/data/server-config.data.ts @@ -9,6 +9,7 @@ import { ServerConfigResponse, ThirdPartyServerConfigResponse, EnvironmentServerConfigResponse, + PushSettingsConfigResponse, } from "../response/server-config.response"; export class ServerConfigData { @@ -18,6 +19,7 @@ export class ServerConfigData { environment?: EnvironmentServerConfigData; utcDate: string; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; + push: PushSettingsConfigData; settings: ServerSettings; constructor(serverConfigResponse: Partial) { @@ -32,6 +34,9 @@ export class ServerConfigData { : null; this.featureStates = serverConfigResponse?.featureStates; this.settings = new ServerSettings(serverConfigResponse.settings); + this.push = serverConfigResponse?.push + ? new PushSettingsConfigData(serverConfigResponse.push) + : null; } static fromJSON(obj: Jsonify): ServerConfigData { @@ -42,6 +47,20 @@ export class ServerConfigData { } } +export class PushSettingsConfigData { + pushTechnology: number; + vapidPublicKey?: string; + + constructor(response: Partial) { + this.pushTechnology = response.pushTechnology; + this.vapidPublicKey = response.vapidPublicKey; + } + + static fromJSON(obj: Jsonify): PushSettingsConfigData { + return Object.assign(new PushSettingsConfigData({}), obj); + } +} + export class ThirdPartyServerConfigData { name: string; url: string; diff --git a/libs/common/src/platform/models/domain/domain-base.spec.ts b/libs/common/src/platform/models/domain/domain-base.spec.ts index 80a4e5e8606c..0c13f9a21190 100644 --- a/libs/common/src/platform/models/domain/domain-base.spec.ts +++ b/libs/common/src/platform/models/domain/domain-base.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { makeEncString, makeSymmetricCryptoKey } from "../../../../spec"; -import { EncryptService } from "../../abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { Utils } from "../../misc/utils"; import Domain from "./domain-base"; diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index 110a1dc7208e..192034254b96 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { ConditionalExcept, ConditionalKeys, Constructor } from "type-fest"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { View } from "../../../models/view/view"; -import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString } from "./enc-string"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index b4916b9f70a8..3b2586fc22f2 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -1,10 +1,9 @@ import { mock, MockProxy } from "jest-mock-extended"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { makeEncString, makeStaticByteArray } from "../../../../spec"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { UserKey, OrgKey } from "../../../types/key"; import { EncryptionType } from "../../enums"; diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index f148664a4f94..a8fee428b130 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify, Opaque } from "type-fest"; -import { EncryptService } from "../../abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../enums"; import { Encrypted } from "../../interfaces/encrypted"; import { Utils } from "../../misc/utils"; diff --git a/libs/common/src/platform/models/response/server-config.response.ts b/libs/common/src/platform/models/response/server-config.response.ts index cae0603ea1e9..afe98c2c349b 100644 --- a/libs/common/src/platform/models/response/server-config.response.ts +++ b/libs/common/src/platform/models/response/server-config.response.ts @@ -11,6 +11,7 @@ export class ServerConfigResponse extends BaseResponse { server: ThirdPartyServerConfigResponse; environment: EnvironmentServerConfigResponse; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; + push: PushSettingsConfigResponse; settings: ServerSettings; constructor(response: any) { @@ -25,10 +26,27 @@ export class ServerConfigResponse extends BaseResponse { this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server")); this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment")); this.featureStates = this.getResponseProperty("FeatureStates"); + this.push = new PushSettingsConfigResponse(this.getResponseProperty("Push")); this.settings = new ServerSettings(this.getResponseProperty("Settings")); } } +export class PushSettingsConfigResponse extends BaseResponse { + pushTechnology: number; + vapidPublicKey: string; + + constructor(data: any = null) { + super(data); + + if (data == null) { + return; + } + + this.pushTechnology = this.getResponseProperty("PushTechnology"); + this.vapidPublicKey = this.getResponseProperty("VapidPublicKey"); + } +} + export class EnvironmentServerConfigResponse extends BaseResponse { cloudRegion: Region; vault: string; diff --git a/libs/common/src/platform/notifications/index.ts b/libs/common/src/platform/notifications/index.ts new file mode 100644 index 000000000000..b1b842f51528 --- /dev/null +++ b/libs/common/src/platform/notifications/index.ts @@ -0,0 +1 @@ +export { NotificationsService } from "./notifications.service"; diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts new file mode 100644 index 000000000000..e24069a9fbef --- /dev/null +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts @@ -0,0 +1,316 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, Subject } from "rxjs"; + +import { LogoutReason } from "@bitwarden/auth/common"; + +import { awaitAsync } from "../../../../spec"; +import { Matrix } from "../../../../spec/matrix"; +import { AccountService } from "../../../auth/abstractions/account.service"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { NotificationType } from "../../../enums"; +import { NotificationResponse } from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { AppIdService } from "../../abstractions/app-id.service"; +import { Environment, EnvironmentService } from "../../abstractions/environment.service"; +import { LogService } from "../../abstractions/log.service"; +import { MessageSender } from "../../messaging"; +import { SupportStatus } from "../../misc/support-status"; +import { SyncService } from "../../sync"; + +import { + DefaultNotificationsService, + DISABLED_NOTIFICATIONS_URL, +} from "./default-notifications.service"; +import { SignalRConnectionService, SignalRNotification } from "./signalr-connection.service"; +import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; +import { WorkerWebPushConnectionService } from "./worker-webpush-connection.service"; + +describe("NotificationsService", () => { + let syncService: MockProxy; + let appIdService: MockProxy; + let environmentService: MockProxy; + let logoutCallback: jest.Mock, [logoutReason: LogoutReason]>; + let messagingService: MockProxy; + let accountService: MockProxy; + let signalRNotificationConnectionService: MockProxy; + let authService: MockProxy; + let webPushNotificationConnectionService: MockProxy; + + let activeAccount: BehaviorSubject>; + + let environment: BehaviorSubject>; + + let authStatusGetter: (userId: UserId) => BehaviorSubject; + + let webPushSupportGetter: (userId: UserId) => BehaviorSubject>; + + let signalrNotificationGetter: ( + userId: UserId, + notificationsUrl: string, + ) => Subject; + + let sut: DefaultNotificationsService; + + beforeEach(() => { + syncService = mock(); + appIdService = mock(); + environmentService = mock(); + logoutCallback = jest.fn, [logoutReason: LogoutReason]>(); + messagingService = mock(); + accountService = mock(); + signalRNotificationConnectionService = mock(); + authService = mock(); + webPushNotificationConnectionService = mock(); + + activeAccount = new BehaviorSubject>(null); + accountService.activeAccount$ = activeAccount.asObservable(); + + environment = new BehaviorSubject>({ + getNotificationsUrl: () => "https://notifications.bitwarden.com", + } as Environment); + + environmentService.environment$ = environment; + + authStatusGetter = Matrix.autoMockMethod( + authService.authStatusFor$, + () => new BehaviorSubject(AuthenticationStatus.LoggedOut), + ); + + webPushSupportGetter = Matrix.autoMockMethod( + webPushNotificationConnectionService.supportStatus$, + () => + new BehaviorSubject>({ + type: "not-supported", + reason: "test", + }), + ); + + signalrNotificationGetter = Matrix.autoMockMethod( + signalRNotificationConnectionService.connect$, + () => new Subject(), + ); + + sut = new DefaultNotificationsService( + mock(), + syncService, + appIdService, + environmentService, + logoutCallback, + messagingService, + accountService, + signalRNotificationConnectionService, + authService, + webPushNotificationConnectionService, + ); + }); + + const mockUser1 = "user1" as UserId; + const mockUser2 = "user2" as UserId; + + function emitActiveUser(userId: UserId) { + if (userId == null) { + activeAccount.next(null); + } else { + activeAccount.next({ id: userId, email: "email", name: "Test Name", emailVerified: true }); + } + } + + function emitNotificationUrl(url: string) { + environment.next({ + getNotificationsUrl: () => url, + } as Environment); + } + + const expectNotification = ( + notification: readonly [NotificationResponse, UserId], + expectedUser: UserId, + expectedType: NotificationType, + ) => { + const [actualNotification, actualUser] = notification; + expect(actualUser).toBe(expectedUser); + expect(actualNotification.type).toBe(expectedType); + }; + + it("emits notifications through WebPush when supported", async () => { + const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(2))); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + + const webPush = mock(); + const webPushSubject = new Subject(); + webPush.notifications$ = webPushSubject; + + webPushSupportGetter(mockUser1).next({ type: "supported", service: webPush }); + webPushSubject.next(new NotificationResponse({ type: NotificationType.SyncFolderCreate })); + webPushSubject.next(new NotificationResponse({ type: NotificationType.SyncFolderDelete })); + + const notifications = await notificationsPromise; + expectNotification(notifications[0], mockUser1, NotificationType.SyncFolderCreate); + expectNotification(notifications[1], mockUser1, NotificationType.SyncFolderDelete); + }); + + it("switches to SignalR when web push is not supported.", async () => { + const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(2))); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + + const webPush = mock(); + const webPushSubject = new Subject(); + webPush.notifications$ = webPushSubject; + + webPushSupportGetter(mockUser1).next({ type: "supported", service: webPush }); + webPushSubject.next(new NotificationResponse({ type: NotificationType.SyncFolderCreate })); + + emitActiveUser(mockUser2); + authStatusGetter(mockUser2).next(AuthenticationStatus.Unlocked); + // Second user does not support web push + webPushSupportGetter(mockUser2).next({ type: "not-supported", reason: "test" }); + + signalrNotificationGetter(mockUser2, "http://test.example.com").next({ + type: "ReceiveMessage", + message: new NotificationResponse({ type: NotificationType.SyncCipherUpdate }), + }); + + const notifications = await notificationsPromise; + expectNotification(notifications[0], mockUser1, NotificationType.SyncFolderCreate); + expectNotification(notifications[1], mockUser2, NotificationType.SyncCipherUpdate); + }); + + it("switches to WebPush when it becomes supported.", async () => { + const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(2))); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + webPushSupportGetter(mockUser1).next({ type: "not-supported", reason: "test" }); + + signalrNotificationGetter(mockUser1, "http://test.example.com").next({ + type: "ReceiveMessage", + message: new NotificationResponse({ type: NotificationType.AuthRequest }), + }); + + const webPush = mock(); + const webPushSubject = new Subject(); + webPush.notifications$ = webPushSubject; + + webPushSupportGetter(mockUser1).next({ type: "supported", service: webPush }); + webPushSubject.next(new NotificationResponse({ type: NotificationType.SyncLoginDelete })); + + const notifications = await notificationsPromise; + expectNotification(notifications[0], mockUser1, NotificationType.AuthRequest); + expectNotification(notifications[1], mockUser1, NotificationType.SyncLoginDelete); + }); + + it("does not emit SignalR heartbeats", async () => { + const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(1))); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + webPushSupportGetter(mockUser1).next({ type: "not-supported", reason: "test" }); + + signalrNotificationGetter(mockUser1, "http://test.example.com").next({ type: "Heartbeat" }); + signalrNotificationGetter(mockUser1, "http://test.example.com").next({ + type: "ReceiveMessage", + message: new NotificationResponse({ type: NotificationType.AuthRequestResponse }), + }); + + const notifications = await notificationsPromise; + expectNotification(notifications[0], mockUser1, NotificationType.AuthRequestResponse); + }); + + it.each([ + { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked }, + { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked }, + { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked }, + { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Unlocked }, + ])( + "does not re-connect when the user transitions from $initialStatus to $updatedStatus", + async ({ initialStatus, updatedStatus }) => { + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(initialStatus); + webPushSupportGetter(mockUser1).next({ type: "not-supported", reason: "test" }); + + const notificationsSubscriptions = sut.notifications$.subscribe(); + await awaitAsync(1); + + authStatusGetter(mockUser1).next(updatedStatus); + await awaitAsync(1); + + expect(signalRNotificationConnectionService.connect$).toHaveBeenCalledTimes(1); + expect(signalRNotificationConnectionService.connect$).toHaveBeenCalledWith( + mockUser1, + "http://test.example.com", + ); + notificationsSubscriptions.unsubscribe(); + }, + ); + + it.each([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked])( + "connects when a user transitions from logged out to %s", + async (newStatus: AuthenticationStatus) => { + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.LoggedOut); + webPushSupportGetter(mockUser1).next({ type: "not-supported", reason: "test" }); + + const notificationsSubscriptions = sut.notifications$.subscribe(); + await awaitAsync(1); + + authStatusGetter(mockUser1).next(newStatus); + await awaitAsync(1); + + expect(signalRNotificationConnectionService.connect$).toHaveBeenCalledTimes(1); + expect(signalRNotificationConnectionService.connect$).toHaveBeenCalledWith( + mockUser1, + "http://test.example.com", + ); + notificationsSubscriptions.unsubscribe(); + }, + ); + + it("does not connect to any notification stream when notifications are disabled through special url", () => { + const subscription = sut.notifications$.subscribe(); + emitActiveUser(mockUser1); + emitNotificationUrl(DISABLED_NOTIFICATIONS_URL); + + expect(signalRNotificationConnectionService.connect$).not.toHaveBeenCalled(); + expect(webPushNotificationConnectionService.supportStatus$).not.toHaveBeenCalled(); + + subscription.unsubscribe(); + }); + + it("does not connect to any notification stream when there is no active user", () => { + const subscription = sut.notifications$.subscribe(); + emitActiveUser(null); + + expect(signalRNotificationConnectionService.connect$).not.toHaveBeenCalled(); + expect(webPushNotificationConnectionService.supportStatus$).not.toHaveBeenCalled(); + + subscription.unsubscribe(); + }); + + it("does not reconnect if the same notification url is emitted", async () => { + const subscription = sut.notifications$.subscribe(); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + + await awaitAsync(1); + + expect(webPushNotificationConnectionService.supportStatus$).toHaveBeenCalledTimes(1); + emitNotificationUrl("http://test.example.com"); + + await awaitAsync(1); + + expect(webPushNotificationConnectionService.supportStatus$).toHaveBeenCalledTimes(1); + subscription.unsubscribe(); + }); +}); diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.ts new file mode 100644 index 000000000000..c6b330857a49 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.ts @@ -0,0 +1,238 @@ +import { + BehaviorSubject, + catchError, + distinctUntilChanged, + EMPTY, + filter, + map, + mergeMap, + Observable, + switchMap, +} from "rxjs"; + +import { LogoutReason } from "@bitwarden/auth/common"; + +import { AccountService } from "../../../auth/abstractions/account.service"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { NotificationType } from "../../../enums"; +import { + NotificationResponse, + SyncCipherNotification, + SyncFolderNotification, + SyncSendNotification, +} from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; +import { AppIdService } from "../../abstractions/app-id.service"; +import { EnvironmentService } from "../../abstractions/environment.service"; +import { LogService } from "../../abstractions/log.service"; +import { MessagingService } from "../../abstractions/messaging.service"; +import { supportSwitch } from "../../misc/support-status"; +import { NotificationsService as NotificationsServiceAbstraction } from "../notifications.service"; + +import { ReceiveMessage, SignalRConnectionService } from "./signalr-connection.service"; +import { WebPushConnectionService } from "./webpush-connection.service"; + +export const DISABLED_NOTIFICATIONS_URL = "http://-"; + +export class DefaultNotificationsService implements NotificationsServiceAbstraction { + notifications$: Observable; + + private activitySubject = new BehaviorSubject<"active" | "inactive">("active"); + + constructor( + private readonly logService: LogService, + private syncService: SyncService, + private appIdService: AppIdService, + private environmentService: EnvironmentService, + private logoutCallback: (logoutReason: LogoutReason, userId: UserId) => Promise, + private messagingService: MessagingService, + private readonly accountService: AccountService, + private readonly signalRConnectionService: SignalRConnectionService, + private readonly authService: AuthService, + private readonly webPushConnectionService: WebPushConnectionService, + ) { + this.notifications$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + distinctUntilChanged(), + switchMap((activeAccountId) => { + if (activeAccountId == null) { + // We don't emit notifications for inactive accounts currently + return EMPTY; + } + + return this.userNotifications$(activeAccountId).pipe( + map((notification) => [notification, activeAccountId] as const), + ); + }), + ); + } + + /** + * Retrieves a stream of push notifications for the given user. + * @param userId The user id of the user to get the push notifications for. + */ + private userNotifications$(userId: UserId) { + return this.environmentService.environment$.pipe( + map((env) => env.getNotificationsUrl()), + distinctUntilChanged(), + switchMap((notificationsUrl) => { + if (notificationsUrl === DISABLED_NOTIFICATIONS_URL) { + return EMPTY; + } + + return this.userNotificationsHelper$(userId, notificationsUrl); + }), + ); + } + + private userNotificationsHelper$(userId: UserId, notificationsUrl: string) { + return this.hasAccessToken$(userId).pipe( + switchMap((hasAccessToken) => { + if (!hasAccessToken) { + return EMPTY; + } + + return this.activitySubject; + }), + switchMap((activityStatus) => { + if (activityStatus === "inactive") { + return EMPTY; + } + + return this.webPushConnectionService.supportStatus$(userId); + }), + supportSwitch({ + supported: (service) => + service.notifications$.pipe( + catchError((err: unknown) => { + this.logService.warning("Issue with web push, falling back to SignalR", err); + return this.connectSignalR$(userId, notificationsUrl); + }), + ), + notSupported: () => this.connectSignalR$(userId, notificationsUrl), + }), + ); + } + + private connectSignalR$(userId: UserId, notificationsUrl: string) { + return this.signalRConnectionService.connect$(userId, notificationsUrl).pipe( + filter((n) => n.type === "ReceiveMessage"), + map((n) => (n as ReceiveMessage).message), + ); + } + + private hasAccessToken$(userId: UserId) { + return this.authService.authStatusFor$(userId).pipe( + map( + (authStatus) => + authStatus === AuthenticationStatus.Locked || + authStatus === AuthenticationStatus.Unlocked, + ), + distinctUntilChanged(), + ); + } + + private async processNotification(notification: NotificationResponse, userId: UserId) { + const appId = await this.appIdService.getAppId(); + if (notification == null || notification.contextId === appId) { + return; + } + + const payloadUserId = notification.payload?.userId || notification.payload?.UserId; + if (payloadUserId != null && payloadUserId !== userId) { + return; + } + + switch (notification.type) { + case NotificationType.SyncCipherCreate: + case NotificationType.SyncCipherUpdate: + await this.syncService.syncUpsertCipher( + notification.payload as SyncCipherNotification, + notification.type === NotificationType.SyncCipherUpdate, + ); + break; + case NotificationType.SyncCipherDelete: + case NotificationType.SyncLoginDelete: + await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); + break; + case NotificationType.SyncFolderCreate: + case NotificationType.SyncFolderUpdate: + await this.syncService.syncUpsertFolder( + notification.payload as SyncFolderNotification, + notification.type === NotificationType.SyncFolderUpdate, + userId, + ); + break; + case NotificationType.SyncFolderDelete: + await this.syncService.syncDeleteFolder( + notification.payload as SyncFolderNotification, + userId, + ); + break; + case NotificationType.SyncVault: + case NotificationType.SyncCiphers: + case NotificationType.SyncSettings: + await this.syncService.fullSync(false); + + break; + case NotificationType.SyncOrganizations: + // An organization update may not have bumped the user's account revision date, so force a sync + await this.syncService.fullSync(true); + break; + case NotificationType.SyncOrgKeys: + await this.syncService.fullSync(true); + this.activitySubject.next("inactive"); // Force a disconnect + this.activitySubject.next("active"); // Allow a reconnect + break; + case NotificationType.LogOut: + this.logService.info("[Notifications Service] Received logout notification"); + await this.logoutCallback("logoutNotification", userId); + break; + case NotificationType.SyncSendCreate: + case NotificationType.SyncSendUpdate: + await this.syncService.syncUpsertSend( + notification.payload as SyncSendNotification, + notification.type === NotificationType.SyncSendUpdate, + ); + break; + case NotificationType.SyncSendDelete: + await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); + break; + case NotificationType.AuthRequest: + { + this.messagingService.send("openLoginApproval", { + notificationId: notification.payload.id, + }); + } + break; + case NotificationType.SyncOrganizationStatusChanged: + await this.syncService.fullSync(true); + break; + case NotificationType.SyncOrganizationCollectionSettingChanged: + await this.syncService.fullSync(true); + break; + default: + break; + } + } + + startListening() { + return this.notifications$ + .pipe( + mergeMap(async ([notification, userId]) => this.processNotification(notification, userId)), + ) + .subscribe({ + error: (e: unknown) => this.logService.warning("Error in notifications$ observable", e), + }); + } + + reconnectFromActivity(): void { + this.activitySubject.next("active"); + } + + disconnectFromInactivity(): void { + this.activitySubject.next("inactive"); + } +} diff --git a/libs/common/src/platform/notifications/internal/index.ts b/libs/common/src/platform/notifications/internal/index.ts new file mode 100644 index 000000000000..067320ee56c1 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/index.ts @@ -0,0 +1,8 @@ +export * from "./worker-webpush-connection.service"; +export * from "./signalr-connection.service"; +export * from "./default-notifications.service"; +export * from "./noop-notifications.service"; +export * from "./unsupported-webpush-connection.service"; +export * from "./webpush-connection.service"; +export * from "./websocket-webpush-connection.service"; +export * from "./web-push-notifications-api.service"; diff --git a/libs/common/src/platform/notifications/internal/noop-notifications.service.ts b/libs/common/src/platform/notifications/internal/noop-notifications.service.ts new file mode 100644 index 000000000000..f79cabfca8a9 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/noop-notifications.service.ts @@ -0,0 +1,23 @@ +import { Subscription } from "rxjs"; + +import { LogService } from "../../abstractions/log.service"; +import { NotificationsService } from "../notifications.service"; + +export class NoopNotificationsService implements NotificationsService { + constructor(private logService: LogService) {} + + startListening(): Subscription { + this.logService.info( + "Initializing no-op notification service, no push notifications will be received", + ); + return Subscription.EMPTY; + } + + reconnectFromActivity(): void { + this.logService.info("Reconnecting notification service from activity"); + } + + disconnectFromInactivity(): void { + this.logService.info("Disconnecting notification service from inactivity"); + } +} diff --git a/libs/common/src/platform/notifications/internal/signalr-connection.service.ts b/libs/common/src/platform/notifications/internal/signalr-connection.service.ts new file mode 100644 index 000000000000..e5d210266c07 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/signalr-connection.service.ts @@ -0,0 +1,125 @@ +import { + HttpTransportType, + HubConnectionBuilder, + HubConnectionState, + ILogger, + LogLevel, +} from "@microsoft/signalr"; +import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; +import { Observable, Subscription } from "rxjs"; + +import { ApiService } from "../../../abstractions/api.service"; +import { NotificationResponse } from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { LogService } from "../../abstractions/log.service"; + +// 2 Minutes +const MIN_RECONNECT_TIME = 2 * 60 * 1000; +// 5 Minutes +const MAX_RECONNECT_TIME = 5 * 60 * 1000; + +export type Heartbeat = { type: "Heartbeat" }; +export type ReceiveMessage = { type: "ReceiveMessage"; message: NotificationResponse }; + +export type SignalRNotification = Heartbeat | ReceiveMessage; + +class SignalRLogger implements ILogger { + constructor(private readonly logService: LogService) {} + + log(logLevel: LogLevel, message: string): void { + switch (logLevel) { + case LogLevel.Critical: + this.logService.error(message); + break; + case LogLevel.Error: + this.logService.error(message); + break; + case LogLevel.Warning: + this.logService.warning(message); + break; + case LogLevel.Information: + this.logService.info(message); + break; + case LogLevel.Debug: + this.logService.debug(message); + break; + } + } +} + +export class SignalRConnectionService { + constructor( + private readonly apiService: ApiService, + private readonly logService: LogService, + ) {} + + connect$(userId: UserId, notificationsUrl: string) { + return new Observable((subsciber) => { + const connection = new HubConnectionBuilder() + .withUrl(notificationsUrl + "/hub", { + accessTokenFactory: () => this.apiService.getActiveBearerToken(), + skipNegotiation: true, + transport: HttpTransportType.WebSockets, + }) + .withHubProtocol(new MessagePackHubProtocol()) + .configureLogging(new SignalRLogger(this.logService)) + .build(); + + connection.on("ReceiveMessage", (data: any) => { + subsciber.next({ type: "ReceiveMessage", message: new NotificationResponse(data) }); + }); + + connection.on("Heartbeat", () => { + subsciber.next({ type: "Heartbeat" }); + }); + + let reconnectSubscription: Subscription | null = null; + + // Create schedule reconnect function + const scheduleReconnect = (): Subscription => { + if ( + connection == null || + connection.state !== HubConnectionState.Disconnected || + (reconnectSubscription != null && !reconnectSubscription.closed) + ) { + return Subscription.EMPTY; + } + + const randomTime = this.random(); + const timeoutHandler = setTimeout(() => { + connection + .start() + .then(() => (reconnectSubscription = null)) + .catch(() => { + reconnectSubscription = scheduleReconnect(); + }); + }, randomTime); + + return new Subscription(() => clearTimeout(timeoutHandler)); + }; + + connection.onclose((error) => { + reconnectSubscription = scheduleReconnect(); + }); + + // Start connection + connection.start().catch(() => { + reconnectSubscription = scheduleReconnect(); + }); + + return () => { + connection?.stop().catch((error) => { + this.logService.error("Error while stopping SignalR connection", error); + // TODO: Does calling stop call `onclose`? + reconnectSubscription?.unsubscribe(); + }); + }; + }); + } + + private random() { + return ( + Math.floor(Math.random() * (MAX_RECONNECT_TIME - MIN_RECONNECT_TIME + 1)) + MIN_RECONNECT_TIME + ); + } +} diff --git a/libs/common/src/platform/notifications/internal/unsupported-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/unsupported-webpush-connection.service.ts new file mode 100644 index 000000000000..0016a8829499 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/unsupported-webpush-connection.service.ts @@ -0,0 +1,15 @@ +import { Observable, of } from "rxjs"; + +import { UserId } from "../../../types/guid"; +import { SupportStatus } from "../../misc/support-status"; + +import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; + +/** + * An implementation of {@see WebPushConnectionService} for clients that do not have support for WebPush + */ +export class UnsupportedWebPushConnectionService implements WebPushConnectionService { + supportStatus$(userId: UserId): Observable> { + return of({ type: "not-supported", reason: "client-not-supported" }); + } +} diff --git a/libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts b/libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts new file mode 100644 index 000000000000..b824b8c7d65b --- /dev/null +++ b/libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts @@ -0,0 +1,25 @@ +import { ApiService } from "../../../abstractions/api.service"; +import { AppIdService } from "../../abstractions/app-id.service"; + +import { WebPushRequest } from "./web-push.request"; + +export class WebPushNotificationsApiService { + constructor( + private readonly apiService: ApiService, + private readonly appIdService: AppIdService, + ) {} + + /** + * Posts a device-user association to the server and ensures it's installed for push notifications + */ + async putSubscription(pushSubscription: PushSubscriptionJSON): Promise { + const request = WebPushRequest.from(pushSubscription); + await this.apiService.send( + "POST", + `/devices/identifier/${await this.appIdService.getAppId()}/web-push-auth`, + request, + true, + false, + ); + } +} diff --git a/libs/common/src/platform/notifications/internal/web-push.request.ts b/libs/common/src/platform/notifications/internal/web-push.request.ts new file mode 100644 index 000000000000..c63759863243 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/web-push.request.ts @@ -0,0 +1,13 @@ +export class WebPushRequest { + endpoint: string | undefined; + p256dh: string | undefined; + auth: string | undefined; + + static from(pushSubscription: PushSubscriptionJSON): WebPushRequest { + const result = new WebPushRequest(); + result.endpoint = pushSubscription.endpoint; + result.p256dh = pushSubscription.keys?.p256dh; + result.auth = pushSubscription.keys?.auth; + return result; + } +} diff --git a/libs/common/src/platform/notifications/internal/webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/webpush-connection.service.ts new file mode 100644 index 000000000000..17ef87ea83ed --- /dev/null +++ b/libs/common/src/platform/notifications/internal/webpush-connection.service.ts @@ -0,0 +1,13 @@ +import { Observable } from "rxjs"; + +import { NotificationResponse } from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { SupportStatus } from "../../misc/support-status"; + +export interface WebPushConnector { + notifications$: Observable; +} + +export abstract class WebPushConnectionService { + abstract supportStatus$(userId: UserId): Observable>; +} diff --git a/libs/common/src/platform/notifications/internal/websocket-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/websocket-webpush-connection.service.ts new file mode 100644 index 000000000000..7a25fb4ce506 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/websocket-webpush-connection.service.ts @@ -0,0 +1,12 @@ +import { Observable, of } from "rxjs"; + +import { UserId } from "../../../types/guid"; +import { SupportStatus } from "../../misc/support-status"; + +import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; + +export class WebSocketWebPushConnectionService implements WebPushConnectionService { + supportStatus$(userId: UserId): Observable> { + return of({ type: "not-supported", reason: "work-in-progress" }); + } +} diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts new file mode 100644 index 000000000000..631c624d667e --- /dev/null +++ b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts @@ -0,0 +1,168 @@ +import { + concat, + concatMap, + defer, + distinctUntilChanged, + fromEvent, + map, + Observable, + Subject, + Subscription, + switchMap, +} from "rxjs"; + +import { PushTechnology } from "../../../enums/push-technology.enum"; +import { NotificationResponse } from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { ConfigService } from "../../abstractions/config/config.service"; +import { SupportStatus } from "../../misc/support-status"; +import { Utils } from "../../misc/utils"; + +import { WebPushNotificationsApiService } from "./web-push-notifications-api.service"; +import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; + +// Ref: https://w3c.github.io/push-api/#the-pushsubscriptionchange-event +interface PushSubscriptionChangeEvent { + readonly newSubscription?: PushSubscription; + readonly oldSubscription?: PushSubscription; +} + +// Ref: https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData +interface PushMessageData { + json(): any; +} + +// Ref: https://developer.mozilla.org/en-US/docs/Web/API/PushEvent +interface PushEvent { + data: PushMessageData; +} + +/** + * An implementation for connecting to web push based notifications running in a Worker. + */ +export class WorkerWebPushConnectionService implements WebPushConnectionService { + private pushEvent = new Subject(); + private pushChangeEvent = new Subject(); + + constructor( + private readonly configService: ConfigService, + private readonly webPushApiService: WebPushNotificationsApiService, + private readonly serviceWorkerRegistration: ServiceWorkerRegistration, + ) {} + + start(): Subscription { + const subscription = new Subscription(() => { + this.pushEvent.complete(); + this.pushChangeEvent.complete(); + this.pushEvent = new Subject(); + this.pushChangeEvent = new Subject(); + }); + + const pushEventSubscription = fromEvent(self, "push").subscribe(this.pushEvent); + + const pushChangeEventSubscription = fromEvent( + self, + "pushsubscriptionchange", + ).subscribe(this.pushChangeEvent); + + subscription.add(pushEventSubscription); + subscription.add(pushChangeEventSubscription); + + return subscription; + } + + supportStatus$(userId: UserId): Observable> { + // Check the server config to see if it supports sending WebPush notifications + // FIXME: get config of server for the specified userId, once ConfigService supports it + return this.configService.serverConfig$.pipe( + map((config) => + config?.push?.pushTechnology === PushTechnology.WebPush ? config.push.vapidPublicKey : null, + ), + // No need to re-emit when there is new server config if the vapidPublicKey is still there and the exact same + distinctUntilChanged(), + map((publicKey) => { + if (publicKey == null) { + return { + type: "not-supported", + reason: "server-not-configured", + } satisfies SupportStatus; + } + + return { + type: "supported", + service: new MyWebPushConnector( + publicKey, + userId, + this.webPushApiService, + this.serviceWorkerRegistration, + this.pushEvent, + this.pushChangeEvent, + ), + } satisfies SupportStatus; + }), + ); + } +} + +class MyWebPushConnector implements WebPushConnector { + notifications$: Observable; + + constructor( + private readonly vapidPublicKey: string, + private readonly userId: UserId, + private readonly webPushApiService: WebPushNotificationsApiService, + private readonly serviceWorkerRegistration: ServiceWorkerRegistration, + private readonly pushEvent$: Observable, + private readonly pushChangeEvent$: Observable, + ) { + this.notifications$ = this.getOrCreateSubscription$(this.vapidPublicKey).pipe( + concatMap((subscription) => { + return defer(() => { + if (subscription == null) { + throw new Error("Expected a non-null subscription."); + } + return this.webPushApiService.putSubscription(subscription.toJSON()); + }).pipe( + switchMap(() => this.pushEvent$), + map((e) => new NotificationResponse(e.data.json().data)), + ); + }), + ); + } + + private async pushManagerSubscribe(key: string) { + return await this.serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: key, + }); + } + + private getOrCreateSubscription$(key: string) { + return concat( + defer(async () => { + const existingSubscription = + await this.serviceWorkerRegistration.pushManager.getSubscription(); + + if (existingSubscription == null) { + return await this.pushManagerSubscribe(key); + } + + const subscriptionKey = Utils.fromBufferToUrlB64( + // REASON: `Utils.fromBufferToUrlB64` handles null by returning null back to it. + // its annotation should be updated and then this assertion can be removed. + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + existingSubscription.options?.applicationServerKey!, + ); + + if (subscriptionKey !== key) { + // There is a subscription, but it's not for the current server, unsubscribe and then make a new one + await existingSubscription.unsubscribe(); + return await this.pushManagerSubscribe(key); + } + + return existingSubscription; + }), + this.pushChangeEvent$.pipe(map((event) => event.newSubscription)), + ); + } +} diff --git a/libs/common/src/platform/notifications/notifications.service.ts b/libs/common/src/platform/notifications/notifications.service.ts new file mode 100644 index 000000000000..aa4ff2a57a64 --- /dev/null +++ b/libs/common/src/platform/notifications/notifications.service.ts @@ -0,0 +1,18 @@ +import { Subscription } from "rxjs"; + +/** + * A service offering abilities to interact with push notifications from the server. + */ +export abstract class NotificationsService { + /** + * Starts automatic listening and processing of notifications, should only be called once per application, + * or you will risk notifications being processed multiple times. + */ + abstract startListening(): Subscription; + // TODO: Delete this method in favor of an `ActivityService` that notifications can depend on. + // https://bitwarden.atlassian.net/browse/PM-14264 + abstract reconnectFromActivity(): void; + // TODO: Delete this method in favor of an `ActivityService` that notifications can depend on. + // https://bitwarden.atlassian.net/browse/PM-14264 + abstract disconnectFromInactivity(): void; +} diff --git a/libs/common/src/platform/services/container.service.ts b/libs/common/src/platform/services/container.service.ts index c3e727a2e1e6..1428b2bbd7c7 100644 --- a/libs/common/src/platform/services/container.service.ts +++ b/libs/common/src/platform/services/container.service.ts @@ -1,7 +1,6 @@ -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; -import { EncryptService } from "../abstractions/encrypt.service"; +import { KeyService } from "@bitwarden/key-management"; + +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; export class ContainerService { constructor( diff --git a/libs/common/src/platform/services/noop-notifications.service.ts b/libs/common/src/platform/services/noop-notifications.service.ts deleted file mode 100644 index edfeccd322d5..000000000000 --- a/libs/common/src/platform/services/noop-notifications.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NotificationsService as NotificationsServiceAbstraction } from "../../abstractions/notifications.service"; -import { LogService } from "../abstractions/log.service"; - -export class NoopNotificationsService implements NotificationsServiceAbstraction { - constructor(private logService: LogService) {} - - init(): Promise { - this.logService.info( - "Initializing no-op notification service, no push notifications will be received", - ); - return Promise.resolve(); - } - - updateConnection(sync?: boolean): Promise { - this.logService.info("Updating notification service connection"); - return Promise.resolve(); - } - - reconnectFromActivity(): Promise { - this.logService.info("Reconnecting notification service from activity"); - return Promise.resolve(); - } - - disconnectFromInactivity(): Promise { - this.logService.info("Disconnecting notification service from inactivity"); - return Promise.resolve(); - } -} diff --git a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts index 8e99af2efed6..fc55cc83ac85 100644 --- a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts +++ b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts @@ -1,19 +1,19 @@ import * as sdk from "@bitwarden/sdk-internal"; -import * as module from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; /** - * Directly imports the Bitwarden SDK and initializes it. - * - * **Warning**: This requires WASM support and will fail if the environment does not support it. + * Default SDK client factory. */ export class DefaultSdkClientFactory implements SdkClientFactory { + /** + * Initializes a Bitwarden client. Assumes the SDK is already loaded. + * @param args Bitwarden client constructor parameters + * @returns A BitwardenClient + */ async createSdkClient( ...args: ConstructorParameters ): Promise { - (sdk as any).init(module); - return Promise.resolve(new sdk.BitwardenClient(...args)); } } diff --git a/libs/common/src/platform/services/sdk/default-sdk-load.service.ts b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts new file mode 100644 index 000000000000..eff641f0351e --- /dev/null +++ b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts @@ -0,0 +1,15 @@ +import * as sdk from "@bitwarden/sdk-internal"; +import * as bitwardenModule from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; + +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; + +/** + * Directly imports the Bitwarden SDK and initializes it. + * + * **Warning**: This requires WASM support and will fail if the environment does not support it. + */ +export class DefaultSdkLoadService implements SdkLoadService { + async load(): Promise { + (sdk as any).init(bitwardenModule); + } +} diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts index c5917e0230fa..e8dfde863eca 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -10,6 +10,7 @@ import { UserKey } from "../../../types/key"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; +import { Rc } from "../../misc/reference-counting/rc"; import { EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; @@ -75,15 +76,14 @@ describe("DefaultSdkService", () => { }); it("creates an SDK client when called the first time", async () => { - const result = await firstValueFrom(service.userClient$(userId)); + await firstValueFrom(service.userClient$(userId)); - expect(result).toBe(mockClient); expect(sdkClientFactory.createSdkClient).toHaveBeenCalled(); }); it("does not create an SDK client when called the second time with same userId", async () => { - const subject_1 = new BehaviorSubject(undefined); - const subject_2 = new BehaviorSubject(undefined); + const subject_1 = new BehaviorSubject | undefined>(undefined); + const subject_2 = new BehaviorSubject | undefined>(undefined); // Use subjects to ensure the subscription is kept alive service.userClient$(userId).subscribe(subject_1); @@ -92,14 +92,14 @@ describe("DefaultSdkService", () => { // Wait for the next tick to ensure all async operations are done await new Promise(process.nextTick); - expect(subject_1.value).toBe(mockClient); - expect(subject_2.value).toBe(mockClient); + expect(subject_1.value.take().value).toBe(mockClient); + expect(subject_2.value.take().value).toBe(mockClient); expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1); }); it("destroys the SDK client when all subscriptions are closed", async () => { - const subject_1 = new BehaviorSubject(undefined); - const subject_2 = new BehaviorSubject(undefined); + const subject_1 = new BehaviorSubject | undefined>(undefined); + const subject_2 = new BehaviorSubject | undefined>(undefined); const subscription_1 = service.userClient$(userId).subscribe(subject_1); const subscription_2 = service.userClient$(userId).subscribe(subject_2); await new Promise(process.nextTick); @@ -107,6 +107,7 @@ describe("DefaultSdkService", () => { subscription_1.unsubscribe(); subscription_2.unsubscribe(); + await new Promise(process.nextTick); expect(mockClient.free).toHaveBeenCalledTimes(1); }); @@ -114,7 +115,7 @@ describe("DefaultSdkService", () => { const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey); keyService.userKey$.calledWith(userId).mockReturnValue(userKey$); - const subject = new BehaviorSubject(undefined); + const subject = new BehaviorSubject | undefined>(undefined); service.userClient$(userId).subscribe(subject); await new Promise(process.nextTick); diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index e9cecbb15dc5..516334c7fb46 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -30,10 +30,11 @@ import { PlatformUtilsService } from "../../abstractions/platform-utils.service" import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; import { SdkService } from "../../abstractions/sdk/sdk.service"; import { compareValues } from "../../misc/compare-values"; +import { Rc } from "../../misc/reference-counting/rc"; import { EncryptedString } from "../../models/domain/enc-string"; export class DefaultSdkService implements SdkService { - private sdkClientCache = new Map>(); + private sdkClientCache = new Map>>(); client$ = this.environmentService.environment$.pipe( concatMap(async (env) => { @@ -58,7 +59,7 @@ export class DefaultSdkService implements SdkService { private userAgent: string = null, ) {} - userClient$(userId: UserId): Observable { + userClient$(userId: UserId): Observable | undefined> { // TODO: Figure out what happens when the user logs out if (this.sdkClientCache.has(userId)) { return this.sdkClientCache.get(userId); @@ -88,32 +89,31 @@ export class DefaultSdkService implements SdkService { // switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value. switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => { // Create our own observable to be able to implement clean-up logic - return new Observable((subscriber) => { - let client: BitwardenClient; - + return new Observable>((subscriber) => { const createAndInitializeClient = async () => { if (privateKey == null || userKey == null) { return undefined; } const settings = this.toSettings(env); - client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); + const client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys); return client; }; + let client: Rc; createAndInitializeClient() .then((c) => { - client = c; - subscriber.next(c); + client = c === undefined ? undefined : new Rc(c); + subscriber.next(client); }) .catch((e) => { subscriber.error(e); }); - return () => client?.free(); + return () => client?.markForDisposal(); }); }), tap({ diff --git a/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts new file mode 100644 index 000000000000..60dac4f21f15 --- /dev/null +++ b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts @@ -0,0 +1,7 @@ +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; + +export class NoopSdkLoadService extends SdkLoadService { + async load() { + return; + } +} diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts index 16b3968045aa..84511d1e71a6 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts @@ -1,8 +1,7 @@ import { mock } from "jest-mock-extended"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { DefaultKeyService } from "../../../../key-management/src/key.service"; +import { DefaultKeyService } from "@bitwarden/key-management"; + import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; import { UserKey } from "../../types/key"; diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.ts index a8947a49f457..bf64c13b0609 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts @@ -1,6 +1,5 @@ -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { UserId } from "../../types/guid"; import { KeySuffixOptions } from "../enums"; diff --git a/libs/common/src/platform/state/global-state.ts b/libs/common/src/platform/state/global-state.ts index b0f19c53faa5..b2ac634df241 100644 --- a/libs/common/src/platform/state/global-state.ts +++ b/libs/common/src/platform/state/global-state.ts @@ -9,7 +9,7 @@ import { StateUpdateOptions } from "./state-update-options"; export interface GlobalState { /** * Method for allowing you to manipulate state in an additive way. - * @param configureState callback for how you want manipulate this section of state + * @param configureState callback for how you want to manipulate this section of state * @param options Defaults given by @see {module:state-update-options#DEFAULT_OPTIONS} * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null @@ -18,13 +18,13 @@ export interface GlobalState { * Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state. */ update: ( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options?: StateUpdateOptions, - ) => Promise; + ) => Promise; /** * An observable stream of this state, the first emission of this will be the current state on disk * and subsequent updates will be from an update to that state. */ - state$: Observable; + state$: Observable; } diff --git a/libs/common/src/platform/state/implementations/default-single-user-state.ts b/libs/common/src/platform/state/implementations/default-single-user-state.ts index 4cfba1ffa952..1dafd3aecada 100644 --- a/libs/common/src/platform/state/implementations/default-single-user-state.ts +++ b/libs/common/src/platform/state/implementations/default-single-user-state.ts @@ -16,7 +16,7 @@ export class DefaultSingleUserState extends StateBase> implements SingleUserState { - readonly combinedState$: Observable>; + readonly combinedState$: Observable>; constructor( readonly userId: UserId, diff --git a/libs/common/src/platform/state/implementations/default-state.provider.ts b/libs/common/src/platform/state/implementations/default-state.provider.ts index f86ba11b268e..317957679790 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.ts @@ -54,9 +54,9 @@ export class DefaultStateProvider implements StateProvider { async setUserState( userKeyDefinition: UserKeyDefinition, - value: T, + value: T | null, userId?: UserId, - ): Promise<[UserId, T]> { + ): Promise<[UserId, T | null]> { if (userId) { return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)]; } else { diff --git a/libs/common/src/platform/state/implementations/state-base.ts b/libs/common/src/platform/state/implementations/state-base.ts index 567de957e539..578720a2281b 100644 --- a/libs/common/src/platform/state/implementations/state-base.ts +++ b/libs/common/src/platform/state/implementations/state-base.ts @@ -1,12 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { - Observable, - ReplaySubject, defer, filter, firstValueFrom, merge, + Observable, + ReplaySubject, share, switchMap, tap, @@ -22,13 +22,13 @@ import { ObservableStorageService, } from "../../abstractions/storage.service"; import { DebugOptions } from "../key-definition"; -import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options"; +import { populateOptionsWithDefault, StateUpdateOptions } from "../state-update-options"; import { getStoredValue } from "./util"; // The parts of a KeyDefinition this class cares about to make it work type KeyDefinitionRequirements = { - deserializer: (jsonState: Jsonify) => T; + deserializer: (jsonState: Jsonify) => T | null; cleanupDelayMs: number; debug: Required; }; @@ -36,7 +36,7 @@ type KeyDefinitionRequirements = { export abstract class StateBase> { private updatePromise: Promise; - readonly state$: Observable; + readonly state$: Observable; constructor( protected readonly key: StorageKey, @@ -86,9 +86,9 @@ export abstract class StateBase> } async update( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options: StateUpdateOptions = {}, - ): Promise { + ): Promise { options = populateOptionsWithDefault(options); if (this.updatePromise != null) { await this.updatePromise; @@ -96,17 +96,16 @@ export abstract class StateBase> try { this.updatePromise = this.internalUpdate(configureState, options); - const newState = await this.updatePromise; - return newState; + return await this.updatePromise; } finally { this.updatePromise = null; } } private async internalUpdate( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options: StateUpdateOptions, - ): Promise { + ): Promise { const currentState = await this.getStateForUpdate(); const combinedDependencies = options.combineLatestWith != null @@ -122,7 +121,7 @@ export abstract class StateBase> return newState; } - protected async doStorageSave(newState: T, oldState: T) { + protected async doStorageSave(newState: T | null, oldState: T) { if (this.keyDefinition.debug.enableUpdateLogging) { this.logService.info( `Updating '${this.key}' from ${oldState == null ? "null" : "non-null"} to ${newState == null ? "null" : "non-null"}`, diff --git a/libs/common/src/platform/state/implementations/util.ts b/libs/common/src/platform/state/implementations/util.ts index f3d57fbafc4a..0a9d76f6da5a 100644 --- a/libs/common/src/platform/state/implementations/util.ts +++ b/libs/common/src/platform/state/implementations/util.ts @@ -5,12 +5,11 @@ import { AbstractStorageService } from "../../abstractions/storage.service"; export async function getStoredValue( key: string, storage: AbstractStorageService, - deserializer: (jsonValue: Jsonify) => T, + deserializer: (jsonValue: Jsonify) => T | null, ) { if (storage.valuesRequireDeserialization) { const jsonValue = await storage.get>(key); - const value = deserializer(jsonValue); - return value; + return deserializer(jsonValue); } else { const value = await storage.get(key); return value ?? null; diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index a270fc3e1a22..519e98ef52d3 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -42,7 +42,7 @@ export type KeyDefinitionOptions = { * @param jsonValue The JSON object representation of your state. * @returns The fully typed version of your state. */ - readonly deserializer: (jsonValue: Jsonify) => T; + readonly deserializer: (jsonValue: Jsonify) => T | null; /** * The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed. * Defaults to 1000ms. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 483a8c050d32..c7901bc34e24 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -29,9 +29,20 @@ export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition( web: "disk-local", }, ); -export const AC_BANNERS_DISMISSED_DISK = new StateDefinition("acBannersDismissed", "disk", { - web: "disk-local", -}); +export const ACCOUNT_DEPROVISIONING_BANNER_DISK = new StateDefinition( + "showAccountDeprovisioningBanner", + "disk", + { + web: "disk-local", + }, +); +export const DELETE_MANAGED_USER_WARNING = new StateDefinition( + "showDeleteManagedUserWarning", + "disk", + { + web: "disk-local", + }, +); // Billing export const BILLING_DISK = new StateDefinition("billing", "disk"); @@ -187,3 +198,4 @@ export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition( }, ); export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk"); +export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); diff --git a/libs/common/src/platform/state/state.provider.ts b/libs/common/src/platform/state/state.provider.ts index 44736500afcf..dc8cb3e93598 100644 --- a/libs/common/src/platform/state/state.provider.ts +++ b/libs/common/src/platform/state/state.provider.ts @@ -60,9 +60,9 @@ export abstract class StateProvider { */ abstract setUserState( keyDefinition: UserKeyDefinition, - value: T, + value: T | null, userId?: UserId, - ): Promise<[UserId, T]>; + ): Promise<[UserId, T | null]>; /** @see{@link ActiveUserStateProvider.get} */ abstract getActive(userKeyDefinition: UserKeyDefinition): ActiveUserState; diff --git a/libs/common/src/platform/state/user-state.ts b/libs/common/src/platform/state/user-state.ts index 44bc8732544b..26fa6f83fa3b 100644 --- a/libs/common/src/platform/state/user-state.ts +++ b/libs/common/src/platform/state/user-state.ts @@ -12,10 +12,11 @@ export interface UserState { readonly state$: Observable; /** Emits a stream of tuples, with the first element being a user id and the second element being the data for that user. */ - readonly combinedState$: Observable>; + readonly combinedState$: Observable>; } export const activeMarker: unique symbol = Symbol("active"); + export interface ActiveUserState extends UserState { readonly [activeMarker]: true; @@ -32,15 +33,16 @@ export interface ActiveUserState extends UserState { * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. - + * * @returns A promise that must be awaited before your next action to ensure the update has been written to state. * Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state. */ readonly update: ( - configureState: (state: T, dependencies: TCombine) => T, + configureState: (state: T | null, dependencies: TCombine) => T | null, options?: StateUpdateOptions, - ) => Promise<[UserId, T]>; + ) => Promise<[UserId, T | null]>; } + export interface SingleUserState extends UserState { readonly userId: UserId; @@ -51,12 +53,12 @@ export interface SingleUserState extends UserState { * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. - + * * @returns A promise that must be awaited before your next action to ensure the update has been written to state. * Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state. */ readonly update: ( - configureState: (state: T, dependencies: TCombine) => T, + configureState: (state: T | null, dependencies: TCombine) => T | null, options?: StateUpdateOptions, - ) => Promise; + ) => Promise; } diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 138c7c033183..b47f0c54208d 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -7,6 +7,7 @@ import { CollectionData, CollectionDetailsResponse, } from "@bitwarden/admin-console/common"; +import { KeyService } from "@bitwarden/key-management"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports @@ -14,9 +15,6 @@ import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/co // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "../../../../auth/src/common/types"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; @@ -197,6 +195,7 @@ export class DefaultSyncService extends CoreSyncService { await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor); await this.tokenService.setSecurityStamp(response.securityStamp, response.id); await this.accountService.setAccountEmailVerified(response.id, response.emailVerified); + await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices); await this.billingAccountProfileStateService.setHasPremium( response.premiumPersonally, diff --git a/libs/common/src/platform/theming/theme-state.service.ts b/libs/common/src/platform/theming/theme-state.service.ts index bb146be4927e..df2c96c49d04 100644 --- a/libs/common/src/platform/theming/theme-state.service.ts +++ b/libs/common/src/platform/theming/theme-state.service.ts @@ -32,7 +32,11 @@ export class DefaultThemeStateService implements ThemeStateService { map(([theme, isExtensionRefresh]) => { // The extension refresh should not allow for Nord or SolarizedDark // Default the user to their system theme - if (isExtensionRefresh && [ThemeType.Nord, ThemeType.SolarizedDark].includes(theme)) { + if ( + isExtensionRefresh && + theme != null && + [ThemeType.Nord, ThemeType.SolarizedDark].includes(theme) + ) { return ThemeType.System; } diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index dc0a8d61f64d..ad59ad0837a8 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -3,9 +3,9 @@ import { firstValueFrom } from "rxjs"; import { - CollectionRequest, CollectionAccessDetailsResponse, CollectionDetailsResponse, + CollectionRequest, CollectionResponse, } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -78,6 +78,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; @@ -103,7 +104,6 @@ import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; -import { TaxRateResponse } from "../billing/models/response/tax-rate.response"; import { DeviceType } from "../enums"; import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum"; import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request"; @@ -158,6 +158,13 @@ export class ApiService implements ApiServiceAbstraction { private deviceType: string; private isWebClient = false; private isDesktopClient = false; + private refreshTokenPromise: Promise | undefined; + + /** + * The message (responseJson.ErrorModel.Message) that comes back from the server when a new device verification is required. + */ + private static readonly NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE = + "new device verification required"; constructor( private tokenService: TokenService, @@ -198,7 +205,12 @@ export class ApiService implements ApiServiceAbstraction { | PasswordTokenRequest | SsoTokenRequest | WebAuthnLoginTokenRequest, - ): Promise { + ): Promise< + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse + > { const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", Accept: "application/json", @@ -246,6 +258,11 @@ export class ApiService implements ApiServiceAbstraction { Object.keys(responseJson.HCaptcha_SiteKey).length ) { return new IdentityCaptchaResponse(responseJson); + } else if ( + response.status === 400 && + responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE + ) { + return new IdentityDeviceVerificationResponse(responseJson); } } @@ -685,7 +702,7 @@ export class ApiService implements ApiServiceAbstraction { } deleteCipherAttachment(id: string, attachmentId: string): Promise { - return this.send("DELETE", "/ciphers/" + id + "/attachment/" + attachmentId, null, true, false); + return this.send("DELETE", "/ciphers/" + id + "/attachment/" + attachmentId, null, true, true); } deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise { @@ -897,11 +914,6 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PlanResponse); } - async getTaxRates(): Promise> { - const r = await this.send("GET", "/plans/sales-tax-rates/", null, true, true); - return new ListResponse(r, TaxRateResponse); - } - // Settings APIs async getSettingsDomains(): Promise { @@ -1722,7 +1734,18 @@ export class ApiService implements ApiServiceAbstraction { ); } - protected async refreshToken(): Promise { + // Keep the running refreshTokenPromise to prevent parallel calls. + protected refreshToken(): Promise { + if (this.refreshTokenPromise === undefined) { + this.refreshTokenPromise = this.internalRefreshToken(); + void this.refreshTokenPromise.finally(() => { + this.refreshTokenPromise = undefined; + }); + } + return this.refreshTokenPromise; + } + + private async internalRefreshToken(): Promise { const refreshToken = await this.tokenService.getRefreshToken(); if (refreshToken != null && refreshToken !== "") { return this.refreshAccessToken(); @@ -1835,7 +1858,7 @@ export class ApiService implements ApiServiceAbstraction { } async send( - method: "GET" | "POST" | "PUT" | "DELETE", + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, body: any, authed: boolean, @@ -1875,7 +1898,7 @@ export class ApiService implements ApiServiceAbstraction { return responseJson; } else if (hasResponse && response.status === 200 && responseIsCsv) { return await response.text(); - } else if (response.status !== 200) { + } else if (response.status !== 200 && response.status !== 204) { const error = await this.handleError(response, false, authed); return Promise.reject(error); } diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index b06985e0ba78..da38ca5bffff 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -1,11 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map, from, zip, Observable } from "rxjs"; +import { firstValueFrom, map, from, zip } 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventType } from "../../enums"; @@ -17,8 +20,6 @@ import { CipherView } from "../../vault/models/view/cipher.view"; import { EVENT_COLLECTION } from "./key-definitions"; export class EventCollectionService implements EventCollectionServiceAbstraction { - private orgIds$: Observable; - constructor( private cipherService: CipherService, private stateProvider: StateProvider, @@ -26,11 +27,11 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private eventUploadService: EventUploadService, private authService: AuthService, private accountService: AccountService, - ) { - this.orgIds$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), - ); - } + ) {} + + private getOrgIds = (orgs: Organization[]): string[] => { + return orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []; + }; /** Adds an event to the active user's event collection * @param eventType the event type to be added @@ -42,14 +43,15 @@ export class EventCollectionService implements EventCollectionServiceAbstraction ciphers: CipherView[], uploadImmediately = false, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); if (!(await this.shouldUpdate(null, eventType, ciphers))) { return; } - const events$ = this.orgIds$.pipe( + const events$ = this.organizationService.organizations$(userId).pipe( + map((orgs) => this.getOrgIds(orgs)), map((orgs) => ciphers .filter((c) => orgs.includes(c.organizationId)) @@ -86,7 +88,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction uploadImmediately = false, organizationId: string = null, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); if (!(await this.shouldUpdate(organizationId, eventType, undefined, cipherId))) { @@ -122,8 +124,14 @@ export class EventCollectionService implements EventCollectionServiceAbstraction ): Promise { const cipher$ = from(this.cipherService.get(cipherId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + const orgIds$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => this.getOrgIds(orgs))); + const [authStatus, orgIds, cipher] = await firstValueFrom( - zip(this.authService.activeAccountStatus$, this.orgIds$, cipher$), + zip(this.authService.activeAccountStatus$, orgIds$, cipher$), ); // The user must be authorized diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts deleted file mode 100644 index f88c904bee1a..000000000000 --- a/libs/common/src/services/notifications.service.ts +++ /dev/null @@ -1,280 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import * as signalR from "@microsoft/signalr"; -import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack"; -import { firstValueFrom, Subscription } from "rxjs"; - -import { LogoutReason } from "@bitwarden/auth/common"; - -import { ApiService } from "../abstractions/api.service"; -import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service"; -import { AuthService } from "../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../auth/enums/authentication-status"; -import { NotificationType } from "../enums"; -import { - NotificationResponse, - SyncCipherNotification, - SyncFolderNotification, - SyncSendNotification, -} from "../models/response/notification.response"; -import { AppIdService } from "../platform/abstractions/app-id.service"; -import { EnvironmentService } from "../platform/abstractions/environment.service"; -import { LogService } from "../platform/abstractions/log.service"; -import { MessagingService } from "../platform/abstractions/messaging.service"; -import { StateService } from "../platform/abstractions/state.service"; -import { ScheduledTaskNames } from "../platform/scheduling/scheduled-task-name.enum"; -import { TaskSchedulerService } from "../platform/scheduling/task-scheduler.service"; -import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction"; - -export class NotificationsService implements NotificationsServiceAbstraction { - private signalrConnection: signalR.HubConnection; - private url: string; - private connected = false; - private inited = false; - private inactive = false; - private reconnectTimerSubscription: Subscription; - private isSyncingOnReconnect = true; - - constructor( - private logService: LogService, - private syncService: SyncService, - private appIdService: AppIdService, - private apiService: ApiService, - private environmentService: EnvironmentService, - private logoutCallback: (logoutReason: LogoutReason) => Promise, - private stateService: StateService, - private authService: AuthService, - private messagingService: MessagingService, - private taskSchedulerService: TaskSchedulerService, - ) { - this.taskSchedulerService.registerTaskHandler( - ScheduledTaskNames.notificationsReconnectTimeout, - () => this.reconnect(this.isSyncingOnReconnect), - ); - this.environmentService.environment$.subscribe(() => { - if (!this.inited) { - 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.init(); - }); - } - - async init(): Promise { - this.inited = false; - this.url = (await firstValueFrom(this.environmentService.environment$)).getNotificationsUrl(); - - // Set notifications server URL to `https://-` to effectively disable communication - // with the notifications server from the client app - if (this.url === "https://-") { - return; - } - - if (this.signalrConnection != null) { - this.signalrConnection.off("ReceiveMessage"); - this.signalrConnection.off("Heartbeat"); - await this.signalrConnection.stop(); - this.connected = false; - this.signalrConnection = null; - } - - this.signalrConnection = new signalR.HubConnectionBuilder() - .withUrl(this.url + "/hub", { - accessTokenFactory: () => this.apiService.getActiveBearerToken(), - skipNegotiation: true, - transport: signalR.HttpTransportType.WebSockets, - }) - .withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol) - // .configureLogging(signalR.LogLevel.Trace) - .build(); - - this.signalrConnection.on("ReceiveMessage", (data: any) => - this.processNotification(new NotificationResponse(data)), - ); - // eslint-disable-next-line - this.signalrConnection.on("Heartbeat", (data: any) => { - /*console.log('Heartbeat!');*/ - }); - this.signalrConnection.onclose(() => { - this.connected = 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.reconnect(true); - }); - this.inited = true; - if (await this.isAuthedAndUnlocked()) { - await this.reconnect(false); - } - } - - async updateConnection(sync = false): Promise { - if (!this.inited) { - return; - } - try { - if (await this.isAuthedAndUnlocked()) { - await this.reconnect(sync); - } else { - await this.signalrConnection.stop(); - } - } catch (e) { - this.logService.error(e.toString()); - } - } - - async reconnectFromActivity(): Promise { - this.inactive = false; - if (this.inited && !this.connected) { - await this.reconnect(true); - } - } - - async disconnectFromInactivity(): Promise { - this.inactive = true; - if (this.inited && this.connected) { - await this.signalrConnection.stop(); - } - } - - private async processNotification(notification: NotificationResponse) { - const appId = await this.appIdService.getAppId(); - if (notification == null || notification.contextId === appId) { - return; - } - - const isAuthenticated = await this.stateService.getIsAuthenticated(); - const payloadUserId = notification.payload.userId || notification.payload.UserId; - const myUserId = await this.stateService.getUserId(); - if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) { - return; - } - - switch (notification.type) { - case NotificationType.SyncCipherCreate: - case NotificationType.SyncCipherUpdate: - await this.syncService.syncUpsertCipher( - notification.payload as SyncCipherNotification, - notification.type === NotificationType.SyncCipherUpdate, - ); - break; - case NotificationType.SyncCipherDelete: - case NotificationType.SyncLoginDelete: - await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); - break; - case NotificationType.SyncFolderCreate: - case NotificationType.SyncFolderUpdate: - await this.syncService.syncUpsertFolder( - notification.payload as SyncFolderNotification, - notification.type === NotificationType.SyncFolderUpdate, - payloadUserId, - ); - break; - case NotificationType.SyncFolderDelete: - await this.syncService.syncDeleteFolder( - notification.payload as SyncFolderNotification, - payloadUserId, - ); - break; - case NotificationType.SyncVault: - case NotificationType.SyncCiphers: - case NotificationType.SyncSettings: - if (isAuthenticated) { - await this.syncService.fullSync(false); - } - break; - case NotificationType.SyncOrganizations: - if (isAuthenticated) { - // An organization update may not have bumped the user's account revision date, so force a sync - await this.syncService.fullSync(true); - } - break; - case NotificationType.SyncOrgKeys: - if (isAuthenticated) { - await this.syncService.fullSync(true); - // Stop so a reconnect can be made - await this.signalrConnection.stop(); - } - break; - case NotificationType.LogOut: - if (isAuthenticated) { - this.logService.info("[Notifications Service] Received logout notification"); - // 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.logoutCallback("logoutNotification"); - } - break; - case NotificationType.SyncSendCreate: - case NotificationType.SyncSendUpdate: - await this.syncService.syncUpsertSend( - notification.payload as SyncSendNotification, - notification.type === NotificationType.SyncSendUpdate, - ); - break; - case NotificationType.SyncSendDelete: - await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); - break; - case NotificationType.AuthRequest: - { - this.messagingService.send("openLoginApproval", { - notificationId: notification.payload.id, - }); - } - break; - case NotificationType.SyncOrganizationStatusChanged: - if (isAuthenticated) { - await this.syncService.fullSync(true); - } - break; - case NotificationType.SyncOrganizationCollectionSettingChanged: - if (isAuthenticated) { - await this.syncService.fullSync(true); - } - break; - default: - break; - } - } - - private async reconnect(sync: boolean) { - this.reconnectTimerSubscription?.unsubscribe(); - - if (this.connected || !this.inited || this.inactive) { - return; - } - const authedAndUnlocked = await this.isAuthedAndUnlocked(); - if (!authedAndUnlocked) { - return; - } - - try { - await this.signalrConnection.start(); - this.connected = true; - if (sync) { - await this.syncService.fullSync(false); - } - } catch (e) { - this.logService.error(e); - } - - if (!this.connected) { - this.isSyncingOnReconnect = sync; - this.reconnectTimerSubscription = this.taskSchedulerService.setTimeout( - ScheduledTaskNames.notificationsReconnectTimeout, - this.random(120000, 300000), - ); - } - } - - private async isAuthedAndUnlocked() { - const authStatus = await this.authService.getAuthStatus(); - return authStatus >= AuthenticationStatus.Unlocked; - } - - private random(min: number, max: number) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; - } -} diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 77ed6c960abc..78c0bc43331e 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -6,11 +6,8 @@ import { FakeUserDecryptionOptions as UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { BiometricStateService } from "@bitwarden/key-management"; +import { BiometricStateService, KeyService } from "@bitwarden/key-management"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index ffc8b6e01445..813f187043bd 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -19,11 +19,8 @@ import { PinServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { BiometricStateService } from "@bitwarden/key-management"; +import { BiometricStateService, KeyService } from "@bitwarden/key-management"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; diff --git a/libs/common/src/state-migrations/.eslintrc.json b/libs/common/src/state-migrations/.eslintrc.json deleted file mode 100644 index 4b66f0a32faf..000000000000 --- a/libs/common/src/state-migrations/.eslintrc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "overrides": [ - { - "files": ["*"], - "rules": { - "import/no-restricted-paths": [ - "error", - { - "basePath": "libs/common/src/state-migrations", - "zones": [ - { - "target": "./", - "from": "../", - // Relative to from, not basePath - "except": ["state-migrations"], - "message": "State migrations should rarely import from the greater codebase. If you need to import from another location, take into account the likelihood of change in that code and consider copying to the migration instead." - } - ] - } - ] - } - } - ] -} diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 81b1016a53d6..b409f52d9369 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -66,13 +66,15 @@ import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-sett import { MoveFinalDesktopSettingsMigrator } from "./migrations/66-move-final-desktop-settings"; import { RemoveUnassignedItemsBannerDismissed } from "./migrations/67-remove-unassigned-items-banner-dismissed"; import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date"; +import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; +import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 68; +export const CURRENT_VERSION = 70; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -142,7 +144,9 @@ export function createMigrationBuilder() { .with(ForwarderOptionsMigrator, 64, 65) .with(MoveFinalDesktopSettingsMigrator, 65, 66) .with(RemoveUnassignedItemsBannerDismissed, 66, 67) - .with(MoveLastSyncDate, 67, CURRENT_VERSION); + .with(MoveLastSyncDate, 67, 68) + .with(MigrateIncorrectFolderKey, 68, 69) + .with(RemoveAcBannersDismissed, 69, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts new file mode 100644 index 000000000000..e5dec943f78f --- /dev/null +++ b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts @@ -0,0 +1,98 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { MigrateIncorrectFolderKey } from "./69-migrate-incorrect-folder-key"; + +function exampleJSON() { + return { + global_account_accounts: { + user1: null as any, + user2: null as any, + }, + user_user1_folder_folder: { + // Incorrect "folder" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + user_user2_folder_folder: null as any, + }; +} + +describe("MigrateIncorrectFolderKey", () => { + const sut = new MigrateIncorrectFolderKey(68, 69); + it("migrates data", async () => { + const output = await runMigrator(sut, exampleJSON()); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folders: { + // Correct "folders" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }); + }); + + it("rolls back data", async () => { + const output = await runMigrator( + sut, + { + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folders: { + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }, + "rollback", + ); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folder: { + // Incorrect "folder" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts new file mode 100644 index 000000000000..046c0cf0dfa3 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts @@ -0,0 +1,45 @@ +import { + KeyDefinitionLike, + MigrationHelper, +} from "@bitwarden/common/state-migrations/migration-helper"; +import { Migrator } from "@bitwarden/common/state-migrations/migrator"; + +const BAD_FOLDER_KEY: KeyDefinitionLike = { + key: "folder", // We inadvertently changed the key from "folders" to "folder" + stateDefinition: { + name: "folder", + }, +}; + +const GOOD_FOLDER_KEY: KeyDefinitionLike = { + key: "folders", // We should keep the key as "folders" + stateDefinition: { + name: "folder", + }, +}; + +export class MigrateIncorrectFolderKey extends Migrator<68, 69> { + async migrate(helper: MigrationHelper): Promise { + async function migrateUser(userId: string) { + const value = await helper.getFromUser(userId, BAD_FOLDER_KEY); + if (value != null) { + await helper.setToUser(userId, GOOD_FOLDER_KEY, value); + } + await helper.removeFromUser(userId, BAD_FOLDER_KEY); + } + const users = await helper.getKnownUserIds(); + await Promise.all(users.map((userId) => migrateUser(userId))); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackUser(userId: string) { + const value = await helper.getFromUser(userId, GOOD_FOLDER_KEY); + if (value != null) { + await helper.setToUser(userId, BAD_FOLDER_KEY, value); + } + await helper.removeFromUser(userId, GOOD_FOLDER_KEY); + } + const users = await helper.getKnownUserIds(); + await Promise.all(users.map((userId) => rollbackUser(userId))); + } +} diff --git a/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.spec.ts b/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.spec.ts new file mode 100644 index 000000000000..59f39d195e90 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.spec.ts @@ -0,0 +1,50 @@ +import { runMigrator } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { RemoveAcBannersDismissed } from "./70-remove-ac-banner-dismissed"; + +describe("RemoveAcBannersDismissed", () => { + const sut = new RemoveAcBannersDismissed(69, 70); + + describe("migrate", () => { + it("deletes ac banner from all users", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + user_user1_showProviderClientVaultPrivacyBanner_acBannersDismissed: true, + user_user2_showProviderClientVaultPrivacyBanner_acBannersDismissed: true, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + }); + }); + }); + + describe("rollback", () => { + it("is irreversible", async () => { + await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.ts b/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.ts new file mode 100644 index 000000000000..087994b508f3 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.ts @@ -0,0 +1,23 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +export const SHOW_BANNER_KEY: KeyDefinitionLike = { + key: "acBannersDismissed", + stateDefinition: { name: "showProviderClientVaultPrivacyBanner" }, +}; + +export class RemoveAcBannersDismissed extends Migrator<69, 70> { + async migrate(helper: MigrationHelper): Promise { + await Promise.all( + (await helper.getAccounts()).map(async ({ userId }) => { + if (helper.getFromUser(userId, SHOW_BANNER_KEY) != null) { + await helper.removeFromUser(userId, SHOW_BANNER_KEY); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts index 0b60aef4917d..66edc5a48380 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts @@ -3,7 +3,7 @@ import { BehaviorSubject, Subject } from "rxjs"; import { KeyService } from "@bitwarden/key-management"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "../../types/csprng"; import { OrganizationId, UserId } from "../../types/guid"; diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts index d4a8dec7dc3f..c91181a004ae 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts @@ -14,7 +14,7 @@ import { import { KeyService } from "@bitwarden/key-management"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { OrganizationId, UserId } from "../../types/guid"; import { OrganizationBound, diff --git a/libs/common/src/tools/cryptography/organization-key-encryptor.spec.ts b/libs/common/src/tools/cryptography/organization-key-encryptor.spec.ts index 62c8ea24ae6f..3d93db813892 100644 --- a/libs/common/src/tools/cryptography/organization-key-encryptor.spec.ts +++ b/libs/common/src/tools/cryptography/organization-key-encryptor.spec.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "../../types/csprng"; diff --git a/libs/common/src/tools/cryptography/organization-key-encryptor.ts b/libs/common/src/tools/cryptography/organization-key-encryptor.ts index d3b7dae10f58..31f3db912320 100644 --- a/libs/common/src/tools/cryptography/organization-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/organization-key-encryptor.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrganizationId } from "../../types/guid"; import { OrgKey } from "../../types/key"; diff --git a/libs/common/src/tools/cryptography/user-key-encryptor.spec.ts b/libs/common/src/tools/cryptography/user-key-encryptor.spec.ts index 5b0ee5103cbf..e52190500b0b 100644 --- a/libs/common/src/tools/cryptography/user-key-encryptor.spec.ts +++ b/libs/common/src/tools/cryptography/user-key-encryptor.spec.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "../../types/csprng"; diff --git a/libs/common/src/tools/cryptography/user-key-encryptor.ts b/libs/common/src/tools/cryptography/user-key-encryptor.ts index 296c33ea1dc7..4b7cd1516a06 100644 --- a/libs/common/src/tools/cryptography/user-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/user-key-encryptor.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { UserId } from "../../types/guid"; import { UserKey } from "../../types/key"; diff --git a/libs/common/src/tools/dependencies.ts b/libs/common/src/tools/dependencies.ts index c22e71cff676..befe1ca5406c 100644 --- a/libs/common/src/tools/dependencies.ts +++ b/libs/common/src/tools/dependencies.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; -import { Policy } from "../admin-console/models/domain/policy"; -import { OrganizationId, UserId } from "../types/guid"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction"; import { UserEncryptor } from "./cryptography/user-encryptor.abstraction"; @@ -152,7 +152,8 @@ export type SingleUserDependency = { }; /** A pattern for types that emit values exclusively when the dependency - * emits a message. + * emits a message. Set a type parameter when your method requires contextual + * information when the request is issued. * * Consumers of this dependency should emit when `on$` emits. If `on$` * completes, the consumer should also complete. If `on$` @@ -161,10 +162,10 @@ export type SingleUserDependency = { * @remarks This dependency is useful when you have a nondeterministic * or stateful algorithm that you would like to run when an event occurs. */ -export type OnDependency = { +export type OnDependency = { /** The stream that controls emissions */ - on$: Observable; + on$: Observable; }; /** A pattern for types that emit when a dependency is `true`. diff --git a/libs/common/src/tools/log/default-semantic-logger.spec.ts b/libs/common/src/tools/log/default-semantic-logger.spec.ts new file mode 100644 index 000000000000..8d1dadb66af0 --- /dev/null +++ b/libs/common/src/tools/log/default-semantic-logger.spec.ts @@ -0,0 +1,199 @@ +import { mock } from "jest-mock-extended"; + +import { LogService } from "../../platform/abstractions/log.service"; +import { LogLevelType } from "../../platform/enums"; + +import { DefaultSemanticLogger } from "./default-semantic-logger"; + +const logger = mock(); + +describe("DefaultSemanticLogger", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("debug", () => { + it("writes structural log messages to console.log", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.debug("this is a debug message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, { + message: "this is a debug message", + level: LogLevelType.Debug, + }); + }); + + it("writes structural content to console.log", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.debug({ example: "this is content" }); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, { + content: { example: "this is content" }, + level: LogLevelType.Debug, + }); + }); + + it("writes structural content to console.log with a message", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.info({ example: "this is content" }, "this is a message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, { + content: { example: "this is content" }, + message: "this is a message", + level: LogLevelType.Info, + }); + }); + }); + + describe("info", () => { + it("writes structural log messages to console.log", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.info("this is an info message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, { + message: "this is an info message", + level: LogLevelType.Info, + }); + }); + + it("writes structural content to console.log", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.info({ example: "this is content" }); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, { + content: { example: "this is content" }, + level: LogLevelType.Info, + }); + }); + + it("writes structural content to console.log with a message", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.info({ example: "this is content" }, "this is a message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, { + content: { example: "this is content" }, + message: "this is a message", + level: LogLevelType.Info, + }); + }); + }); + + describe("warn", () => { + it("writes structural log messages to console.warn", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.warn("this is a warning message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, { + message: "this is a warning message", + level: LogLevelType.Warning, + }); + }); + + it("writes structural content to console.warn", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.warn({ example: "this is content" }); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, { + content: { example: "this is content" }, + level: LogLevelType.Warning, + }); + }); + + it("writes structural content to console.warn with a message", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.warn({ example: "this is content" }, "this is a message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, { + content: { example: "this is content" }, + message: "this is a message", + level: LogLevelType.Warning, + }); + }); + }); + + describe("error", () => { + it("writes structural log messages to console.error", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.error("this is an error message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + message: "this is an error message", + level: LogLevelType.Error, + }); + }); + + it("writes structural content to console.error", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.error({ example: "this is content" }); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + content: { example: "this is content" }, + level: LogLevelType.Error, + }); + }); + + it("writes structural content to console.error with a message", () => { + const log = new DefaultSemanticLogger(logger, {}); + + log.error({ example: "this is content" }, "this is a message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + content: { example: "this is content" }, + message: "this is a message", + level: LogLevelType.Error, + }); + }); + }); + + describe("panic", () => { + it("writes structural log messages to console.error before throwing the message", () => { + const log = new DefaultSemanticLogger(logger, {}); + + expect(() => log.panic("this is an error message")).toThrow("this is an error message"); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + message: "this is an error message", + level: LogLevelType.Error, + }); + }); + + it("writes structural log messages to console.error with a message before throwing the message", () => { + const log = new DefaultSemanticLogger(logger, {}); + + expect(() => log.panic({ example: "this is content" }, "this is an error message")).toThrow( + "this is an error message", + ); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + content: { example: "this is content" }, + message: "this is an error message", + level: LogLevelType.Error, + }); + }); + + it("writes structural log messages to console.error with a content before throwing the message", () => { + const log = new DefaultSemanticLogger(logger, {}); + + expect(() => log.panic("this is content", "this is an error message")).toThrow( + "this is an error message", + ); + + expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + content: "this is content", + message: "this is an error message", + level: LogLevelType.Error, + }); + }); + }); +}); diff --git a/libs/common/src/tools/log/default-semantic-logger.ts b/libs/common/src/tools/log/default-semantic-logger.ts new file mode 100644 index 000000000000..1bd62baf126d --- /dev/null +++ b/libs/common/src/tools/log/default-semantic-logger.ts @@ -0,0 +1,65 @@ +import { Jsonify } from "type-fest"; + +import { LogService } from "../../platform/abstractions/log.service"; +import { LogLevelType } from "../../platform/enums"; + +import { SemanticLogger } from "./semantic-logger.abstraction"; + +/** Sends semantic logs to the console. + * @remarks the behavior of this logger is based on `LogService`; it + * replaces dynamic messages (`%s`) with a JSON-formatted semantic log. + */ +export class DefaultSemanticLogger implements SemanticLogger { + /** Instantiates a console semantic logger + * @param context a static payload that is cloned when the logger + * logs a message. The `messages`, `level`, and `content` fields + * are reserved for use by loggers. + */ + constructor( + private logger: LogService, + context: Jsonify, + ) { + this.context = context && typeof context === "object" ? context : {}; + } + + readonly context: object; + + debug(content: Jsonify, message?: string): void { + this.log(content, LogLevelType.Debug, message); + } + + info(content: Jsonify, message?: string): void { + this.log(content, LogLevelType.Info, message); + } + + warn(content: Jsonify, message?: string): void { + this.log(content, LogLevelType.Warning, message); + } + + error(content: Jsonify, message?: string): void { + this.log(content, LogLevelType.Error, message); + } + + panic(content: Jsonify, message?: string): never { + this.log(content, LogLevelType.Error, message); + const panicMessage = + message ?? (typeof content === "string" ? content : "a fatal error occurred"); + throw new Error(panicMessage); + } + + private log(content: Jsonify, level: LogLevelType, message?: string) { + const log = { + ...this.context, + message, + content: content ?? undefined, + level, + }; + + if (typeof content === "string" && !message) { + log.message = content; + delete log.content; + } + + this.logger.write(level, log); + } +} diff --git a/libs/common/src/tools/log/disabled-semantic-logger.ts b/libs/common/src/tools/log/disabled-semantic-logger.ts new file mode 100644 index 000000000000..054c3ed390bb --- /dev/null +++ b/libs/common/src/tools/log/disabled-semantic-logger.ts @@ -0,0 +1,18 @@ +import { Jsonify } from "type-fest"; + +import { SemanticLogger } from "./semantic-logger.abstraction"; + +/** Disables semantic logs. Still panics. */ +export class DisabledSemanticLogger implements SemanticLogger { + debug(_content: Jsonify, _message?: string): void {} + + info(_content: Jsonify, _message?: string): void {} + + warn(_content: Jsonify, _message?: string): void {} + + error(_content: Jsonify, _message?: string): void {} + + panic(_content: Jsonify, message?: string): never { + throw new Error(message); + } +} diff --git a/libs/common/src/tools/log/factory.ts b/libs/common/src/tools/log/factory.ts new file mode 100644 index 000000000000..d887c4e310aa --- /dev/null +++ b/libs/common/src/tools/log/factory.ts @@ -0,0 +1,30 @@ +import { Jsonify } from "type-fest"; + +import { LogService } from "../../platform/abstractions/log.service"; + +import { DefaultSemanticLogger } from "./default-semantic-logger"; +import { DisabledSemanticLogger } from "./disabled-semantic-logger"; +import { SemanticLogger } from "./semantic-logger.abstraction"; + +/** Instantiates a semantic logger that emits nothing when a message + * is logged. + * @param _context a static payload that is cloned when the logger + * logs a message. The `messages`, `level`, and `content` fields + * are reserved for use by loggers. + */ +export function disabledSemanticLoggerProvider( + _context: Jsonify, +): SemanticLogger { + return new DisabledSemanticLogger(); +} + +/** Instantiates a semantic logger that emits logs to the console. + * @param context a static payload that is cloned when the logger + * logs a message. The `messages`, `level`, and `content` fields + * are reserved for use by loggers. + * @param settings specializes how the semantic logger functions. + * If this is omitted, the logger suppresses debug messages. + */ +export function consoleSemanticLoggerProvider(logger: LogService): SemanticLogger { + return new DefaultSemanticLogger(logger, {}); +} diff --git a/libs/common/src/tools/log/index.ts b/libs/common/src/tools/log/index.ts new file mode 100644 index 000000000000..1f86ca4e4d7d --- /dev/null +++ b/libs/common/src/tools/log/index.ts @@ -0,0 +1,2 @@ +export { disabledSemanticLoggerProvider, consoleSemanticLoggerProvider } from "./factory"; +export { SemanticLogger } from "./semantic-logger.abstraction"; diff --git a/libs/common/src/tools/log/semantic-logger.abstraction.ts b/libs/common/src/tools/log/semantic-logger.abstraction.ts new file mode 100644 index 000000000000..196d1f3f12c2 --- /dev/null +++ b/libs/common/src/tools/log/semantic-logger.abstraction.ts @@ -0,0 +1,94 @@ +import { Jsonify } from "type-fest"; + +/** Semantic/structural logging component */ +export interface SemanticLogger { + /** Logs a message at debug priority. + * Debug messages are used for diagnostics, and are typically disabled + * in production builds. + * @param message - a message to record in the log's `message` field. + */ + debug(message: string): void; + + /** Logs the content at debug priority. + * Debug messages are used for diagnostics, and are typically disabled + * in production builds. + * @param content - JSON content included in the log's `content` field. + * @param message - a message to record in the log's `message` field. + */ + debug(content: Jsonify, message?: string): void; + + /** combined signature for overloaded methods */ + debug(content: Jsonify | string, message?: string): void; + + /** Logs a message at informational priority. + * Information messages are used for status reports. + * @param message - a message to record in the log's `message` field. + */ + info(message: string): void; + + /** Logs the content at informational priority. + * Information messages are used for status reports. + * @param content - JSON content included in the log's `content` field. + * @param message - a message to record in the log's `message` field. + */ + info(content: Jsonify, message?: string): void; + + /** combined signature for overloaded methods */ + info(content: Jsonify | string, message?: string): void; + + /** Logs a message at warn priority. + * Warn messages are used to indicate a operation that may affect system + * stability occurred. + * @param message - a message to record in the log's `message` field. + */ + warn(message: string): void; + + /** Logs the content at warn priority. + * Warn messages are used to indicate a operation that may affect system + * stability occurred. + * @param content - JSON content included in the log's `content` field. + * @param message - a message to record in the log's `message` field. + */ + warn(content: Jsonify, message?: string): void; + + /** combined signature for overloaded methods */ + warn(content: Jsonify | string, message?: string): void; + + /** Logs a message at error priority. + * Error messages are used to indicate a operation that affects system + * stability occurred and the system was able to recover. + * @param message - a message to record in the log's `message` field. + */ + error(message: string): void; + + /** Logs the content at debug priority. + * Error messages are used to indicate a operation that affects system + * stability occurred and the system was able to recover. + * @param content - JSON content included in the log's `content` field. + * @param message - a message to record in the log's `message` field. + */ + error(content: Jsonify, message?: string): void; + + /** combined signature for overloaded methods */ + error(content: Jsonify | string, message?: string): void; + + /** Logs a message at panic priority and throws an error. + * Panic messages are used to indicate a operation that affects system + * stability occurred and the system cannot recover. Panic messages + * log an error and throw an `Error`. + * @param message - a message to record in the log's `message` field. + */ + panic(message: string): never; + + /** Logs the content at debug priority and throws an error. + * Panic messages are used to indicate a operation that affects system + * stability occurred and the system cannot recover. Panic messages + * log an error and throw an `Error`. + * @param content - JSON content included in the log's `content` field. + * @param message - a message to record in the log's `message` field. + */ + panic(content: Jsonify, message?: string): never; + + /** combined signature for overloaded methods */ + panic(content: Jsonify | string, message?: string): never; +} diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index fcc273d41bb8..79f6c03adc8d 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended"; import { KeyService } from "@bitwarden/key-management"; import { makeStaticByteArray, mockEnc } from "../../../../../spec"; -import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../../key-management/crypto/abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../../platform/services/container.service"; import { UserKey } from "../../../../types/key"; diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 662fee02bbf1..26cc0a467086 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -10,7 +10,7 @@ import { awaitAsync, mockAccountServiceWith, } from "../../../../spec"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EnvironmentService } from "../../../platform/abstractions/environment.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 7021c942d44f..1b5e5f6aa31e 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -4,7 +4,7 @@ import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from import { PBKDF2KdfConfig, KeyService } from "@bitwarden/key-management"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { Utils } from "../../../platform/misc/utils"; diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index 59dbe28f9070..5ad498c115ad 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -10,3 +10,4 @@ export type PolicyId = Opaque; export type CipherId = Opaque; export type SendId = Opaque; export type IndexedEntityId = Opaque; +export type SecurityTaskId = Opaque; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 88cd476606d6..0672ae29e912 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -93,7 +93,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; + ) => Promise; shareManyWithServer: ( ciphers: CipherView[], organizationId: string, @@ -154,8 +154,8 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; deleteWithServer: (id: string, asAdmin?: boolean) => Promise; deleteManyWithServer: (ids: string[], asAdmin?: boolean) => Promise; - deleteAttachment: (id: string, attachmentId: string) => Promise; - deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; + deleteAttachment: (id: string, revisionDate: string, attachmentId: string) => Promise; + deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; sortCiphersByLastUsed: (a: CipherView, b: CipherView) => number; sortCiphersByLastUsedThenName: (a: CipherView, b: CipherView) => number; getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number; diff --git a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts index b95dd35e5ec1..1bb4a52e9291 100644 --- a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts @@ -2,11 +2,12 @@ // @ts-strict-ignore import { UserId } from "../../../types/guid"; +import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; import { FolderResponse } from "../../models/response/folder.response"; export class FolderApiServiceAbstraction { - save: (folder: Folder, userId: UserId) => Promise; + save: (folder: Folder, userId: UserId) => Promise; delete: (id: string, userId: UserId) => Promise; get: (id: string) => Promise; deleteAll: (userId: UserId) => Promise; diff --git a/libs/common/src/vault/icon/build-cipher-icon.spec.ts b/libs/common/src/vault/icon/build-cipher-icon.spec.ts new file mode 100644 index 000000000000..8de65390bf7e --- /dev/null +++ b/libs/common/src/vault/icon/build-cipher-icon.spec.ts @@ -0,0 +1,141 @@ +import { CipherType } from "../enums"; +import { CipherView } from "../models/view/cipher.view"; + +import { buildCipherIcon } from "./build-cipher-icon"; + +describe("buildCipherIcon", () => { + const iconServerUrl = "https://icons.example"; + describe("Login cipher", () => { + const cipher = { + type: CipherType.Login, + login: { + uri: "https://test.example", + }, + } as any as CipherView; + + it.each([true, false])("handles android app URIs for showFavicon setting %s", (showFavicon) => { + setUri("androidapp://test.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon); + + expect(iconDetails).toEqual({ + icon: "bwi-android", + image: null, + fallbackImage: "", + imageEnabled: showFavicon, + }); + }); + + it("does not mark as an android app if the protocol is not androidapp", () => { + // This weird URI points to test.androidapp with a default port and path of /.example + setUri("https://test.androidapp://.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: "https://icons.example/test.androidapp/icon.png", + fallbackImage: "images/bwi-globe.png", + imageEnabled: true, + }); + }); + + it.each([true, false])("handles ios app URIs for showFavicon setting %s", (showFavicon) => { + setUri("iosapp://test.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon); + + expect(iconDetails).toEqual({ + icon: "bwi-apple", + image: null, + fallbackImage: "", + imageEnabled: showFavicon, + }); + }); + + it("does not mark as an ios app if the protocol is not iosapp", () => { + // This weird URI points to test.iosapp with a default port and path of /.example + setUri("https://test.iosapp://.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: "https://icons.example/test.iosapp/icon.png", + fallbackImage: "images/bwi-globe.png", + imageEnabled: true, + }); + }); + + const testUris = ["test.example", "https://test.example"]; + + it.each(testUris)("resolves favicon for %s", (uri) => { + setUri(uri); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: "https://icons.example/test.example/icon.png", + fallbackImage: "images/bwi-globe.png", + imageEnabled: true, + }); + }); + + it.each(testUris)("does not resolve favicon for %s if showFavicon is false", () => { + setUri("https://test.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, false); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: undefined, + fallbackImage: "", + imageEnabled: false, + }); + }); + + it("does not resolve a favicon if the URI is missing a `.`", () => { + setUri("test"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: undefined, + fallbackImage: "", + imageEnabled: true, + }); + }); + + it.each(["test.onion", "test.i2p"])("does not resolve a favicon for %s", (uri) => { + setUri(`https://${uri}`); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: null, + fallbackImage: "images/bwi-globe.png", + imageEnabled: true, + }); + }); + + it.each([null, undefined])("does not resolve a favicon if there is no uri", (nullish) => { + setUri(nullish as any as string); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: null, + fallbackImage: "", + imageEnabled: true, + }); + }); + + function setUri(uri: string) { + (cipher.login as { uri: string }).uri = uri; + } + }); +}); diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 8cae7170738e..d1ee1dcc1efb 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -1,10 +1,9 @@ import { mock, MockProxy } from "jest-mock-extended"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index dd79da3086e8..9eadd20f5439 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -1,12 +1,11 @@ import { mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; diff --git a/libs/common/src/vault/models/domain/folder.spec.ts b/libs/common/src/vault/models/domain/folder.spec.ts index 785852b884ea..ff1c38bdd45e 100644 --- a/libs/common/src/vault/models/domain/folder.spec.ts +++ b/libs/common/src/vault/models/domain/folder.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { makeEncString, makeSymmetricCryptoKey, mockEnc, mockFromJson } from "../../../../spec"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; diff --git a/libs/common/src/vault/models/domain/folder.ts b/libs/common/src/vault/models/domain/folder.ts index 93d04607af52..65018e3cf084 100644 --- a/libs/common/src/vault/models/domain/folder.ts +++ b/libs/common/src/vault/models/domain/folder.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index a1ecb473597f..6346f38f0de3 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -2,8 +2,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; import { mockEnc, mockFromJson } from "../../../../spec"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { LoginUriData } from "../data/login-uri.data"; diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 20dbd23065c2..650a1e9dc459 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -142,6 +142,13 @@ export class CipherView implements View, InitializerMetadata { ); } + get canAssignToCollections(): boolean { + if (this.organizationId == null) { + return true; + } + + return this.edit && this.viewPassword; + } /** * Determines if the cipher can be launched in a new browser tab. */ diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index 15e8b03bfad4..37ddfdeaeebe 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -1,11 +1,13 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; +import { Observable, firstValueFrom, of } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "../../admin-console/models/domain/organization"; -import { CollectionId } from "../../types/guid"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; import { CipherView } from "../models/view/cipher.view"; import { @@ -18,6 +20,8 @@ describe("CipherAuthorizationService", () => { const mockCollectionService = mock(); const mockOrganizationService = mock(); + const mockUserId = Utils.newGuid() as UserId; + let mockAccountService: FakeAccountService; // Mock factories const createMockCipher = ( @@ -42,6 +46,7 @@ describe("CipherAuthorizationService", () => { isAdmin = false, editAnyCollection = false, } = {}) => ({ + id: "org1", allowAdminAccessToAllCollectionItems, canEditAllCiphers, canEditUnassignedCiphers, @@ -53,9 +58,11 @@ describe("CipherAuthorizationService", () => { beforeEach(() => { jest.clearAllMocks(); + mockAccountService = mockAccountServiceWith(mockUserId); cipherAuthorizationService = new DefaultCipherAuthorizationService( mockCollectionService, mockOrganizationService, + mockAccountService, ); }); @@ -72,7 +79,9 @@ describe("CipherAuthorizationService", () => { it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ canEditUnassignedCiphers: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization]) as Observable, + ); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(true); @@ -83,11 +92,13 @@ describe("CipherAuthorizationService", () => { it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => { const cipher = createMockCipher("org1", ["col1"]) as CipherView; const organization = createMockOrganization({ canEditAllCiphers: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization]) as Observable, + ); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(true); - expect(mockOrganizationService.get$).toHaveBeenCalledWith("org1"); + expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId); done(); }); }); @@ -95,7 +106,7 @@ describe("CipherAuthorizationService", () => { it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ canEditUnassignedCiphers: false }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(false); @@ -106,8 +117,8 @@ describe("CipherAuthorizationService", () => { it("should return true if activeCollectionId is provided and has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const activeCollectionId = "col1" as CollectionId; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", true), @@ -132,8 +143,8 @@ describe("CipherAuthorizationService", () => { it("should return false if activeCollectionId is provided and manage permission is not present", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const activeCollectionId = "col1" as CollectionId; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -157,8 +168,8 @@ describe("CipherAuthorizationService", () => { it("should return true if any collection has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2", "col3"]) as CipherView; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -182,8 +193,8 @@ describe("CipherAuthorizationService", () => { it("should return false if no collection has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -216,7 +227,9 @@ describe("CipherAuthorizationService", () => { it("should return true for admin users", async () => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ isAdmin: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const result = await firstValueFrom( cipherAuthorizationService.canCloneCipher$(cipher, true), @@ -227,7 +240,9 @@ describe("CipherAuthorizationService", () => { it("should return true for custom user with canEditAnyCollection", async () => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ editAnyCollection: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const result = await firstValueFrom( cipherAuthorizationService.canCloneCipher$(cipher, true), @@ -240,7 +255,9 @@ describe("CipherAuthorizationService", () => { it("should return true if at least one cipher collection has manage permission", async () => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const organization = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const allCollections = [ createMockCollection("col1", true), @@ -257,7 +274,9 @@ describe("CipherAuthorizationService", () => { it("should return false if no collection has manage permission", async () => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const organization = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const allCollections = [ createMockCollection("col1", false), diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index 025d6b1cdc38..fbee3ed86229 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -3,9 +3,10 @@ import { map, Observable, of, shareReplay, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { CollectionId } from "@bitwarden/common/types/guid"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { CollectionId } from "../../types/guid"; import { Cipher } from "../models/domain/cipher"; import { CipherView } from "../models/view/cipher.view"; @@ -51,8 +52,14 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer constructor( private collectionService: CollectionService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} + private organization$ = (cipher: CipherLike) => + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + map((orgs) => orgs.find((org) => org.id === cipher.organizationId)), + ); /** * * {@link CipherAuthorizationService.canDeleteCipher$} @@ -66,7 +73,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organizationService.get$(cipher.organizationId).pipe( + return this.organization$(cipher).pipe( switchMap((organization) => { if (isAdminConsoleAction) { // If the user is an admin, they can delete an unassigned cipher @@ -104,7 +111,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organizationService.get$(cipher.organizationId).pipe( + return this.organization$(cipher).pipe( switchMap((organization) => { // Admins and custom users can always clone when in the Admin Console if ( diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 0d6578f165d9..c59f66729852 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,12 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, map, of } from "rxjs"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { - CipherDecryptionKeys, - KeyService, -} from "../../../../key-management/src/abstractions/key.service"; +import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management"; + import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray } from "../../../spec/utils"; @@ -14,10 +10,10 @@ import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; +import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategy } from "../../models/domain/domain-service"; -import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index fe946fbb0649..9e06d3335c38 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -14,22 +14,21 @@ import { } from "rxjs"; import { SemVer } from "semver"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { FeatureFlag } from "../../enums/feature-flag.enum"; +import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; -import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; import { sequentialize } from "../../platform/misc/sequentialize"; @@ -786,7 +785,7 @@ export class CipherService implements CipherServiceAbstraction { organizationId: string, collectionIds: string[], userId: UserId, - ): Promise { + ): Promise { const attachmentPromises: Promise[] = []; if (cipher.attachments != null) { cipher.attachments.forEach((attachment) => { @@ -806,6 +805,7 @@ export class CipherService implements CipherServiceAbstraction { const response = await this.apiService.putShareCipher(cipher.id, request); const data = new CipherData(response, collectionIds); await this.upsert(data); + return new Cipher(data, cipher.localData); } async shareManyWithServer( @@ -1077,7 +1077,11 @@ export class CipherService implements CipherServiceAbstraction { await this.delete(ids); } - async deleteAttachment(id: string, attachmentId: string): Promise { + async deleteAttachment( + id: string, + revisionDate: string, + attachmentId: string, + ): Promise { let ciphers = await firstValueFrom(this.ciphers$); const cipherId = id as CipherId; // eslint-disable-next-line @@ -1091,6 +1095,10 @@ export class CipherService implements CipherServiceAbstraction { } } + // Deleting the cipher updates the revision date on the server, + // Update the stored `revisionDate` to match + ciphers[cipherId].revisionDate = revisionDate; + await this.clearCache(); await this.encryptedCiphersState.update(() => { if (ciphers == null) { @@ -1098,15 +1106,20 @@ export class CipherService implements CipherServiceAbstraction { } return ciphers; }); + + return ciphers[cipherId]; } - async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { + async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { + let cipherResponse = null; try { - await this.apiService.deleteCipherAttachment(id, attachmentId); + cipherResponse = await this.apiService.deleteCipherAttachment(id, attachmentId); } catch (e) { return Promise.reject((e as ErrorResponse).getSingleMessage()); } - await this.deleteAttachment(id, attachmentId); + const cipherData = CipherData.fromJSON(cipherResponse?.cipher); + + return await this.deleteAttachment(id, cipherData.revisionDate, attachmentId); } sortCiphersByLastUsed(a: CipherView, b: CipherView): number { diff --git a/libs/common/src/vault/services/folder/folder-api.service.ts b/libs/common/src/vault/services/folder/folder-api.service.ts index 9d434cc646d4..fe9c3218a842 100644 --- a/libs/common/src/vault/services/folder/folder-api.service.ts +++ b/libs/common/src/vault/services/folder/folder-api.service.ts @@ -13,7 +13,7 @@ export class FolderApiService implements FolderApiServiceAbstraction { private apiService: ApiService, ) {} - async save(folder: Folder, userId: UserId): Promise { + async save(folder: Folder, userId: UserId): Promise { const request = new FolderRequest(folder); let response: FolderResponse; @@ -26,6 +26,7 @@ export class FolderApiService implements FolderApiServiceAbstraction { const data = new FolderData(response); await this.folderService.upsert(data, userId); + return data; } async delete(id: string, userId: UserId): Promise { diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index cc3aa1946caa..ced4e2dceb74 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -1,14 +1,13 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { makeEncString } from "../../../../spec"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; import { FakeSingleUserState } from "../../../../spec/fake-state"; import { FakeStateProvider } from "../../../../spec/fake-state-provider"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index c21a92fd894a..3d272416fbe7 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -2,12 +2,11 @@ // @ts-strict-ignore import { Observable, Subject, firstValueFrom, map, shareReplay, switchMap, merge } from "rxjs"; -import { EncryptService } from ".././../../platform/abstractions/encrypt.service"; -import { Utils } from ".././../../platform/misc/utils"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; +import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; diff --git a/libs/common/src/vault/services/key-state/folder.state.ts b/libs/common/src/vault/services/key-state/folder.state.ts index 99ad8e5ae356..b3e61f5bf31d 100644 --- a/libs/common/src/vault/services/key-state/folder.state.ts +++ b/libs/common/src/vault/services/key-state/folder.state.ts @@ -6,7 +6,7 @@ import { FolderView } from "../../models/view/folder.view"; export const FOLDER_ENCRYPTED_FOLDERS = UserKeyDefinition.record( FOLDER_DISK, - "folder", + "folders", { deserializer: (obj: Jsonify) => FolderData.fromJSON(obj), clearOn: ["logout"], diff --git a/libs/common/tsconfig.json b/libs/common/tsconfig.json index c28b60e28f8a..2d1379f9c5fe 100644 --- a/libs/common/tsconfig.json +++ b/libs/common/tsconfig.json @@ -6,9 +6,12 @@ "@bitwarden/auth/common": ["../auth/src/common"], // TODO: Remove once circular dependencies in admin-console, auth and key-management are resolved "@bitwarden/common/*": ["../common/src/*"], + // TODO: Remove once billing stops depending on components "@bitwarden/components": ["../components/src"], "@bitwarden/key-management": ["../key-management/src"], - "@bitwarden/platform": ["../platform/src"] + "@bitwarden/platform": ["../platform/src"], + // TODO: Remove once billing stops depending on components + "@bitwarden/ui-common": ["../ui/common/src"] } }, "include": ["src", "spec", "./custom-matchers.d.ts", "../key-management/src/index.ts"], diff --git a/libs/angular/src/directives/a11y-title.directive.ts b/libs/components/src/a11y/a11y-title.directive.ts similarity index 98% rename from libs/angular/src/directives/a11y-title.directive.ts rename to libs/components/src/a11y/a11y-title.directive.ts index f5f016b93c09..c3833f42ed2d 100644 --- a/libs/angular/src/directives/a11y-title.directive.ts +++ b/libs/components/src/a11y/a11y-title.directive.ts @@ -4,6 +4,7 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; @Directive({ selector: "[appA11yTitle]", + standalone: true, }) export class A11yTitleDirective implements OnInit { @Input() set appA11yTitle(title: string) { diff --git a/libs/components/src/a11y/index.ts b/libs/components/src/a11y/index.ts new file mode 100644 index 000000000000..6090fb65d4e5 --- /dev/null +++ b/libs/components/src/a11y/index.ts @@ -0,0 +1 @@ +export * from "./a11y-title.directive"; diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index 7ddb32da2812..b45f750084cd 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -4,10 +4,8 @@ import { action } from "@storybook/addon-actions"; import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { delay, of } from "rxjs"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; import { ButtonModule } from "../button"; import { FormFieldModule } from "../form-field"; diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 76ff702e88b5..0e3dbd6f1b9e 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf, NgClass } from "@angular/common"; +import { NgClass } from "@angular/common"; import { Component, Input, OnChanges } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; @@ -18,9 +18,11 @@ const SizeClasses: Record = { @Component({ selector: "bit-avatar", - template: ``, + template: `@if (src) { + + }`, standalone: true, - imports: [NgIf, NgClass], + imports: [NgClass], }) export class AvatarComponent implements OnChanges { @Input() border = false; diff --git a/libs/components/src/avatar/avatar.stories.ts b/libs/components/src/avatar/avatar.stories.ts index d3a00fbe344c..19a6f86d89c0 100644 --- a/libs/components/src/avatar/avatar.stories.ts +++ b/libs/components/src/avatar/avatar.stories.ts @@ -13,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16994", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26525&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/badge-list/badge-list.component.html b/libs/components/src/badge-list/badge-list.component.html index ebd63117d039..c8aa7b846808 100644 --- a/libs/components/src/badge-list/badge-list.component.html +++ b/libs/components/src/badge-list/badge-list.component.html @@ -1,11 +1,15 @@

- + @for (item of filteredItems; track item; let last = $last) { {{ item }} - , - - - {{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }} - + @if (!last || isFiltered) { + , + } + } + @if (isFiltered) { + + {{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }} + + }
diff --git a/libs/components/src/badge-list/badge-list.component.ts b/libs/components/src/badge-list/badge-list.component.ts index ac8cb3281ab1..86e9a84cb773 100644 --- a/libs/components/src/badge-list/badge-list.component.ts +++ b/libs/components/src/badge-list/badge-list.component.ts @@ -1,16 +1,17 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CommonModule } from "@angular/common"; + import { Component, Input, OnChanges } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BadgeModule, BadgeVariant } from "../badge"; -import { I18nPipe } from "../shared/i18n.pipe"; @Component({ selector: "bit-badge-list", templateUrl: "badge-list.component.html", standalone: true, - imports: [CommonModule, BadgeModule, I18nPipe], + imports: [BadgeModule, I18nPipe], }) export class BadgeListComponent implements OnChanges { private _maxItems: number; diff --git a/libs/components/src/badge-list/badge-list.stories.ts b/libs/components/src/badge-list/badge-list.stories.ts index ede005f6fd67..f69ecde83773 100644 --- a/libs/components/src/badge-list/badge-list.stories.ts +++ b/libs/components/src/badge-list/badge-list.stories.ts @@ -33,7 +33,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16956", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26440&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/badge/badge.stories.ts b/libs/components/src/badge/badge.stories.ts index 5d697f8ad8fc..bff9eec6163e 100644 --- a/libs/components/src/badge/badge.stories.ts +++ b/libs/components/src/badge/badge.stories.ts @@ -18,7 +18,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16956", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26440&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/banner/banner.component.html b/libs/components/src/banner/banner.component.html index 566494eb64a5..1a9d58d342ad 100644 --- a/libs/components/src/banner/banner.component.html +++ b/libs/components/src/banner/banner.component.html @@ -4,21 +4,24 @@ [attr.role]="useAlertRole ? 'status' : null" [attr.aria-live]="useAlertRole ? 'polite' : null" > - + @if (icon) { + + } - + @if (showClose) { + + }
diff --git a/libs/components/src/banner/banner.component.ts b/libs/components/src/banner/banner.component.ts index d3f643299782..a7b710d6a74d 100644 --- a/libs/components/src/banner/banner.component.ts +++ b/libs/components/src/banner/banner.component.ts @@ -3,8 +3,9 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { IconButtonModule } from "../icon-button"; -import { I18nPipe } from "../shared/i18n.pipe"; type BannerTypes = "premium" | "info" | "warning" | "danger"; diff --git a/libs/components/src/banner/banner.stories.ts b/libs/components/src/banner/banner.stories.ts index c75d8b7034ae..105d30bc04a7 100644 --- a/libs/components/src/banner/banner.stories.ts +++ b/libs/components/src/banner/banner.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=2070%3A17207", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26720&t=b5tDKylm5sWm2yKo-4", }, }, args: { diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.html b/libs/components/src/breadcrumbs/breadcrumb.component.html index dd5bac9beb43..bb4dc7cdffe1 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.html +++ b/libs/components/src/breadcrumbs/breadcrumb.component.html @@ -1,3 +1,6 @@ - + @if (icon) { + + } + diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.ts b/libs/components/src/breadcrumbs/breadcrumb.component.ts index ce18bde171f5..53c46a9b24a0 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumb.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf } from "@angular/common"; + import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; import { QueryParamsHandling } from "@angular/router"; @@ -8,7 +8,6 @@ import { QueryParamsHandling } from "@angular/router"; selector: "bit-breadcrumb", templateUrl: "./breadcrumb.component.html", standalone: true, - imports: [NgIf], }) export class BreadcrumbComponent { @Input() diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.html b/libs/components/src/breadcrumbs/breadcrumbs.component.html index 502bb0bb8e73..5205e19cee59 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.html +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.html @@ -1,5 +1,5 @@ - - +@for (breadcrumb of beforeOverflow; track breadcrumb; let last = $last) { + @if (breadcrumb.route) { - - + } + @if (!breadcrumb.route) { - - - - - - + } + @if (!last) { + + } +} +@if (hasOverflow) { + @if (beforeOverflow.length > 0) { + + } - - - + @for (breadcrumb of overflow; track breadcrumb) { + @if (breadcrumb.route) { - - + } + @if (!breadcrumb.route) { - - + } + } - - - + @for (breadcrumb of afterOverflow; track breadcrumb; let last = $last) { + @if (breadcrumb.route) { - - + } + @if (!breadcrumb.route) { - - - - + } + @if (!last) { + + } + } +} diff --git a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts index 9c8ccbccd3f5..eb75a70ad755 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts @@ -35,6 +35,12 @@ export default { ], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26962&t=b5tDKylm5sWm2yKo-4", + }, + }, args: { items: [], show: 3, diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 3654442801c9..6024b0559f20 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -13,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5115%3A26950", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28224&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; @@ -86,16 +86,15 @@ export const DisabledWithAttribute: Story = { render: (args) => ({ props: args, template: ` - + @if (disabled) { - - + } @else { - + } `, }), args: { diff --git a/libs/components/src/callout/callout.component.html b/libs/components/src/callout/callout.component.html index f64e197b9ef2..bb7f918df32d 100644 --- a/libs/components/src/callout/callout.component.html +++ b/libs/components/src/callout/callout.component.html @@ -3,10 +3,14 @@ [ngClass]="calloutClass" [attr.aria-labelledby]="titleId" > -
- - {{ title }} -
+ @if (title) { +
+ @if (icon) { + + } + {{ title }} +
+ }
diff --git a/libs/components/src/callout/callout.stories.ts b/libs/components/src/callout/callout.stories.ts index cb51e96e8dac..3101d4316f18 100644 --- a/libs/components/src/callout/callout.stories.ts +++ b/libs/components/src/callout/callout.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17484", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28300&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts index 37756088e0d1..fdb02f280da0 100644 --- a/libs/components/src/card/card.component.ts +++ b/libs/components/src/card/card.component.ts @@ -1,10 +1,8 @@ -import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component } from "@angular/core"; @Component({ selector: "bit-card", standalone: true, - imports: [CommonModule], template: ``, changeDetection: ChangeDetectionStrategy.OnPush, host: { diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts index b33f5f4a198e..3482eedfd547 100644 --- a/libs/components/src/card/card.stories.ts +++ b/libs/components/src/card/card.stories.ts @@ -34,6 +34,12 @@ export default { (story) => `
${story}
`, ), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28355&t=b5tDKylm5sWm2yKo-4", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/checkbox/checkbox.stories.ts b/libs/components/src/checkbox/checkbox.stories.ts index f3e5a5cb3f7d..9a59897e009c 100644 --- a/libs/components/src/checkbox/checkbox.stories.ts +++ b/libs/components/src/checkbox/checkbox.stories.ts @@ -9,9 +9,7 @@ import { } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { BadgeModule } from "../badge"; import { FormControlModule } from "../form-control"; @@ -83,7 +81,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-35837&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/chip-select/chip-select.component.html b/libs/components/src/chip-select/chip-select.component.html index 81480f107f14..e88200b6e4f9 100644 --- a/libs/components/src/chip-select/chip-select.component.html +++ b/libs/components/src/chip-select/chip-select.component.html @@ -30,78 +30,80 @@ {{ label }} - + @if (!selectedOption) { + + } - -
- - -
- - - - - - + @if (selectedOption) { -
+ } +
+ + + @if (renderedOptions) { +
+ @if (getParent(renderedOptions); as parent) { + + + } + @for (option of renderedOptions.children; track option) { + + } +
+ }
diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index a653d79f83fd..e9be66da7d4e 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -18,9 +18,8 @@ import { import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { compareValues } from "../../../common/src/platform/misc/compare-values"; +import { compareValues } from "@bitwarden/common/platform/misc/compare-values"; + import { ButtonModule } from "../button"; import { IconButtonModule } from "../icon-button"; import { MenuComponent, MenuItemDirective, MenuModule } from "../menu"; @@ -46,6 +45,7 @@ export type ChipSelectOption = Option & { multi: true, }, ], + preserveWhitespaces: false, }) export class ChipSelectComponent implements ControlValueAccessor, AfterViewInit { @ViewChild(MenuComponent) menu: MenuComponent; diff --git a/libs/components/src/chip-select/chip-select.stories.ts b/libs/components/src/chip-select/chip-select.stories.ts index 598fa5b80be9..e9b78235ccb1 100644 --- a/libs/components/src/chip-select/chip-select.stories.ts +++ b/libs/components/src/chip-select/chip-select.stories.ts @@ -30,6 +30,12 @@ export default { ], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-29548&t=b5tDKylm5sWm2yKo-4", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index cbf746e9d735..e48758ca59ac 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgFor, NgIf } from "@angular/common"; + import { Component, HostBinding, Input } from "@angular/core"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -14,18 +14,16 @@ enum CharacterType { @Component({ selector: "bit-color-password", - template: ` - {{ character }} - {{ - i + 1 - }} - `, + template: `@for (character of passwordArray; track character; let i = $index) { + + {{ character }} + @if (showCount) { + {{ i + 1 }} + } + + }`, preserveWhitespaces: false, standalone: true, - imports: [NgFor, NgIf], }) export class ColorPasswordComponent { @Input() password: string = null; diff --git a/libs/components/src/color-password/color-password.stories.ts b/libs/components/src/color-password/color-password.stories.ts index 07418cad7216..bb835d97d4a4 100644 --- a/libs/components/src/color-password/color-password.stories.ts +++ b/libs/components/src/color-password/color-password.stories.ts @@ -14,7 +14,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/6fvTDa3zfvgWdizLQ7nSTP/Numbered-Password", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21540-46261&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/color-password/index.ts b/libs/components/src/color-password/index.ts index 86718f037f77..24870ca75d99 100644 --- a/libs/components/src/color-password/index.ts +++ b/libs/components/src/color-password/index.ts @@ -1 +1,2 @@ export * from "./color-password.module"; +export * from "./color-password.component"; diff --git a/libs/components/src/container/container.component.ts b/libs/components/src/container/container.component.ts index fbd9e40a7ca9..1bcdb8f459b3 100644 --- a/libs/components/src/container/container.component.ts +++ b/libs/components/src/container/container.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; /** @@ -7,7 +6,6 @@ import { Component } from "@angular/core"; @Component({ selector: "bit-container", templateUrl: "container.component.html", - imports: [CommonModule], standalone: true, }) export class ContainerComponent {} diff --git a/libs/angular/src/directives/copy-click.directive.spec.ts b/libs/components/src/copy-click/copy-click.directive.spec.ts similarity index 78% rename from libs/angular/src/directives/copy-click.directive.spec.ts rename to libs/components/src/copy-click/copy-click.directive.spec.ts index 29466f7fbe3d..eab616b141e9 100644 --- a/libs/angular/src/directives/copy-click.directive.spec.ts +++ b/libs/components/src/copy-click/copy-click.directive.spec.ts @@ -3,23 +3,32 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; 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 { ToastService } from "../"; import { CopyClickDirective } from "./copy-click.directive"; @Component({ template: ` - - - - + + + + `, + standalone: true, + imports: [CopyClickDirective], }) class TestCopyClickComponent { - @ViewChild("noToast") noToastButton: ElementRef; - @ViewChild("infoToast") infoToastButton: ElementRef; - @ViewChild("successToast") successToastButton: ElementRef; - @ViewChild("toastWithLabel") toastWithLabelButton: ElementRef; + @ViewChild("noToast") noToastButton!: ElementRef; + @ViewChild("infoToast") infoToastButton!: ElementRef; + @ViewChild("successToast") successToastButton!: ElementRef; + @ViewChild("toastWithLabel") toastWithLabelButton!: ElementRef; } describe("CopyClickDirective", () => { @@ -32,7 +41,7 @@ describe("CopyClickDirective", () => { showToast.mockClear(); await TestBed.configureTestingModule({ - declarations: [CopyClickDirective, TestCopyClickComponent], + imports: [TestCopyClickComponent], providers: [ { provide: I18nService, diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/components/src/copy-click/copy-click.directive.ts similarity index 96% rename from libs/angular/src/directives/copy-click.directive.ts rename to libs/components/src/copy-click/copy-click.directive.ts index ece867c09fd6..f91366360c51 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/components/src/copy-click/copy-click.directive.ts @@ -4,10 +4,12 @@ import { Directive, HostListener, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService, ToastVariant } from "@bitwarden/components"; + +import { ToastService, ToastVariant } from "../"; @Directive({ selector: "[appCopyClick]", + standalone: true, }) export class CopyClickDirective { private _showToast = false; diff --git a/libs/components/src/copy-click/index.ts b/libs/components/src/copy-click/index.ts new file mode 100644 index 000000000000..82b971f1f5c3 --- /dev/null +++ b/libs/components/src/copy-click/index.ts @@ -0,0 +1 @@ +export * from "./copy-click.directive"; diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index 5e938412804e..e7c5a17c308f 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -19,7 +19,7 @@ interface Animal { } @Component({ - template: ``, + template: ``, }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} @@ -42,8 +42,10 @@ class StoryDialogComponent { Animal: {{ animal }} - - + + `, @@ -91,7 +93,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-30495&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 06a7772edbe0..01f059851271 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -13,9 +13,11 @@ class="tw-text-main tw-mb-0 tw-truncate" > {{ title }} - - {{ subtitle }} - + @if (subtitle) { + + {{ subtitle }} + + }

+ @if (showCancelButton) { + + }
diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts index 60b2e1c3a3f4..000262091832 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts @@ -1,7 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; -import { NgIf } from "@angular/common"; import { Component, Inject } from "@angular/core"; import { FormGroup, ReactiveFormsModule } from "@angular/forms"; @@ -39,7 +38,6 @@ const DEFAULT_COLOR: Record = { IconDirective, ButtonComponent, BitFormButtonDirective, - NgIf, ], }) export class SimpleConfigurableDialogComponent { diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts index 4f21b8611b37..87d6eb9fbfc8 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts @@ -12,22 +12,24 @@ import { DialogModule } from "../../dialog.module"; @Component({ template: ` -
-

{{ group.title }}

-
- + @for (group of dialogs; track group) { +
+

{{ group.title }}

+
+ @for (dialog of group.dialogs; track dialog) { + + } +
-
+ } - - {{ dialogCloseResult }} - + @if (showCallout) { + + {{ dialogCloseResult }} + + } `, }) class StoryDialogComponent { @@ -175,7 +177,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21514-19247&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html index 0b56c6287dca..1f154a8d5437 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html @@ -3,12 +3,11 @@ @fadeIn >
- + @if (hasIcon) { - - + } @else { - + }

Open Simple Dialog`, + template: ``, }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} @@ -41,8 +41,10 @@ class StoryDialogComponent { Animal: {{ animal }} - - + + `, @@ -87,7 +89,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21514-19247&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts index d86b56101b04..50016ef358d8 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts @@ -17,7 +17,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21514-19247&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/simple-dialog/types.ts b/libs/components/src/dialog/simple-dialog/types.ts index 4032b499cbee..9724111b4c23 100644 --- a/libs/components/src/dialog/simple-dialog/types.ts +++ b/libs/components/src/dialog/simple-dialog/types.ts @@ -46,7 +46,7 @@ export type SimpleDialogOptions = { * If null is provided, the cancel button will be removed. * * If not localized, pass in a `Translation` */ - cancelButtonText?: string | Translation; + cancelButtonText?: string | Translation | null; /** Whether or not the user can use escape or clicking the backdrop to close the dialog */ disableClose?: boolean; diff --git a/libs/components/src/disclosure/disclosure.stories.ts b/libs/components/src/disclosure/disclosure.stories.ts index 974589a667c1..bb3680c1f3bf 100644 --- a/libs/components/src/disclosure/disclosure.stories.ts +++ b/libs/components/src/disclosure/disclosure.stories.ts @@ -13,6 +13,12 @@ export default { imports: [DisclosureTriggerForDirective, DisclosureComponent, IconButtonModule], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-47329&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/drawer/drawer-header.component.ts b/libs/components/src/drawer/drawer-header.component.ts index 73834b8487e5..de112a448cf9 100644 --- a/libs/components/src/drawer/drawer-header.component.ts +++ b/libs/components/src/drawer/drawer-header.component.ts @@ -1,8 +1,9 @@ import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, HostBinding, input } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { IconButtonModule } from "../icon-button"; -import { I18nPipe } from "../shared/i18n.pipe"; import { TypographyModule } from "../typography"; import { DrawerCloseDirective } from "./drawer-close.directive"; diff --git a/libs/components/src/form-control/form-control.component.html b/libs/components/src/form-control/form-control.component.html index aed8f7e3b242..b15202b0223b 100644 --- a/libs/components/src/form-control/form-control.component.html +++ b/libs/components/src/form-control/form-control.component.html @@ -9,11 +9,17 @@ > - ({{ "required" | i18n }}) + @if (required) { + ({{ "required" | i18n }}) + } - + @if (!hasError) { + + } -
- {{ displayError }} -
+@if (hasError) { +
+ {{ displayError }} +
+} diff --git a/libs/components/src/form-control/form-control.component.ts b/libs/components/src/form-control/form-control.component.ts index 9b87c44157a6..690c00a9dc04 100644 --- a/libs/components/src/form-control/form-control.component.ts +++ b/libs/components/src/form-control/form-control.component.ts @@ -1,12 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; -import { NgClass, NgIf } from "@angular/common"; +import { NgClass } from "@angular/common"; import { Component, ContentChild, HostBinding, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nPipe } from "@bitwarden/ui-common"; -import { I18nPipe } from "../shared/i18n.pipe"; import { TypographyDirective } from "../typography/typography.directive"; import { BitFormControlAbstraction } from "./form-control.abstraction"; @@ -15,7 +15,7 @@ import { BitFormControlAbstraction } from "./form-control.abstraction"; selector: "bit-form-control", templateUrl: "form-control.component.html", standalone: true, - imports: [NgClass, TypographyDirective, NgIf, I18nPipe], + imports: [NgClass, TypographyDirective, I18nPipe], }) export class FormControlComponent { @Input() label: string; diff --git a/libs/components/src/form-control/label.component.html b/libs/components/src/form-control/label.component.html index 64ba1ce95017..2a0a57e35d82 100644 --- a/libs/components/src/form-control/label.component.html +++ b/libs/components/src/form-control/label.component.html @@ -5,10 +5,10 @@ - + @if (isInsideFormControl) { - + } - +@if (!isInsideFormControl) { - +} diff --git a/libs/components/src/form-field/bit-validators.stories.ts b/libs/components/src/form-field/bit-validators.stories.ts index 642ff30bb5a8..748f92f25237 100644 --- a/libs/components/src/form-field/bit-validators.stories.ts +++ b/libs/components/src/form-field/bit-validators.stories.ts @@ -35,7 +35,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/error-summary.component.ts b/libs/components/src/form-field/error-summary.component.ts index beed32a88acf..1709c3078faa 100644 --- a/libs/components/src/form-field/error-summary.component.ts +++ b/libs/components/src/form-field/error-summary.component.ts @@ -1,22 +1,22 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf } from "@angular/common"; + import { Component, Input } from "@angular/core"; import { AbstractControl, UntypedFormGroup } from "@angular/forms"; -import { I18nPipe } from "../shared/i18n.pipe"; +import { I18nPipe } from "@bitwarden/ui-common"; @Component({ selector: "bit-error-summary", - template: ` + template: ` @if (errorCount > 0) { {{ "fieldsNeedAttention" | i18n: errorString }} - `, + }`, host: { class: "tw-block tw-text-danger tw-mt-2", "aria-live": "assertive", }, standalone: true, - imports: [NgIf, I18nPipe], + imports: [I18nPipe], }) export class BitErrorSummary { @Input() diff --git a/libs/components/src/form-field/error-summary.stories.ts b/libs/components/src/form-field/error-summary.stories.ts index 4e1031abaf6b..4b1a30c45b32 100644 --- a/libs/components/src/form-field/error-summary.stories.ts +++ b/libs/components/src/form-field/error-summary.stories.ts @@ -34,7 +34,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/form-field.component.html b/libs/components/src/form-field/form-field.component.html index d2771f424651..bd0998596082 100644 --- a/libs/components/src/form-field/form-field.component.html +++ b/libs/components/src/form-field/form-field.component.html @@ -15,63 +15,65 @@ -
-
-
-
-
-
-
-
-
- -
-
- +
+ +
+
+ +
+
+ +
-
- - +} @else {
- +} - - - - +@switch (input.hasError) { + @case (false) { + + } + @case (true) { + + } +} diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 9f41c6cf6ac1..4f7c2a674834 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -14,10 +14,11 @@ import { signal, } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitHintComponent } from "../form-control/hint.component"; import { BitLabel } from "../form-control/label.component"; import { inputBorderClasses } from "../input/input.directive"; -import { I18nPipe } from "../shared/i18n.pipe"; import { BitErrorComponent } from "./error.component"; import { BitFormFieldControl } from "./form-field-control"; diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index 6d8323e088e4..738ac96bf767 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -108,7 +108,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/index.ts b/libs/components/src/form-field/index.ts index 613ebaf9a9da..0c45f215ec90 100644 --- a/libs/components/src/form-field/index.ts +++ b/libs/components/src/form-field/index.ts @@ -1,4 +1,5 @@ export * from "./form-field.module"; export * from "./form-field.component"; export * from "./form-field-control"; +export * from "./password-input-toggle.directive"; export * as BitValidators from "./bit-validators"; diff --git a/libs/components/src/form-field/multi-select.stories.ts b/libs/components/src/form-field/multi-select.stories.ts index da4776ab0251..8d84aa735bfa 100644 --- a/libs/components/src/form-field/multi-select.stories.ts +++ b/libs/components/src/form-field/multi-select.stories.ts @@ -56,7 +56,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5600%3A24278", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/password-input-toggle.stories.ts b/libs/components/src/form-field/password-input-toggle.stories.ts index 094f939e0ead..d46ec92ab37a 100644 --- a/libs/components/src/form-field/password-input-toggle.stories.ts +++ b/libs/components/src/form-field/password-input-toggle.stories.ts @@ -27,7 +27,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, docs: { description: { diff --git a/libs/components/src/form/form.stories.ts b/libs/components/src/form/form.stories.ts index 23b2cc8cea2c..6aef140fe5fa 100644 --- a/libs/components/src/form/form.stories.ts +++ b/libs/components/src/form/form.stories.ts @@ -64,7 +64,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index 6274bb2b8d17..08c95c5d6410 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -13,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4369%3A16686", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-37011&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 54cdd7928cd2..53454567b7f5 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -6,6 +6,12 @@ import * as GenericIcons from "./icons"; export default { title: "Component Library/Icon", component: BitIconComponent, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-50335&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index ed844520444a..7788f4986bfd 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,3 +1,5 @@ +export { ButtonType } from "./shared/button-like.abstraction"; +export * from "./a11y"; export * from "./async-actions"; export * from "./avatar"; export * from "./badge-list"; @@ -5,13 +7,13 @@ export * from "./badge"; export * from "./banner"; export * from "./breadcrumbs"; export * from "./button"; -export { ButtonType } from "./shared/button-like.abstraction"; export * from "./callout"; export * from "./card"; export * from "./checkbox"; export * from "./chip-select"; export * from "./color-password"; export * from "./container"; +export * from "./copy-click"; export * from "./dialog"; export * from "./disclosure"; export * from "./drawer"; diff --git a/libs/components/src/input/index.ts b/libs/components/src/input/index.ts index 9713d2b9192f..6bd64495910e 100644 --- a/libs/components/src/input/index.ts +++ b/libs/components/src/input/index.ts @@ -1,2 +1,3 @@ export * from "./input.module"; export * from "./autofocus.directive"; +export * from "./input.directive"; diff --git a/libs/components/src/item/item-content.component.html b/libs/components/src/item/item-content.component.html index 6f900c5c6e15..4010970dc9e8 100644 --- a/libs/components/src/item/item-content.component.html +++ b/libs/components/src/item/item-content.component.html @@ -6,14 +6,26 @@ bitTypography="body2" class="tw-text-main tw-truncate tw-inline-flex tw-items-center tw-gap-1.5 tw-w-full" > -
+
-
+
diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts index f6cc3f133adf..2a6e06291fd2 100644 --- a/libs/components/src/item/item-content.component.ts +++ b/libs/components/src/item/item-content.component.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CommonModule } from "@angular/common"; + +import { NgClass } from "@angular/common"; import { AfterContentChecked, ChangeDetectionStrategy, Component, ElementRef, + Input, signal, ViewChild, } from "@angular/core"; @@ -15,7 +17,7 @@ import { TypographyModule } from "../typography"; @Component({ selector: "bit-item-content, [bit-item-content]", standalone: true, - imports: [CommonModule, TypographyModule], + imports: [TypographyModule, NgClass], templateUrl: `item-content.component.html`, host: { class: @@ -32,6 +34,13 @@ export class ItemContentComponent implements AfterContentChecked { protected endSlotHasChildren = signal(false); + /** + * Determines whether text will truncate or wrap. + * + * Default behavior is truncation. + */ + @Input() truncate = true; + ngAfterContentChecked(): void { this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0); } diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts index 97a804843736..1ef4a4af1faf 100644 --- a/libs/components/src/item/item.component.ts +++ b/libs/components/src/item/item.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, @@ -14,7 +13,7 @@ import { ItemActionComponent } from "./item-action.component"; @Component({ selector: "bit-item", standalone: true, - imports: [CommonModule, ItemActionComponent], + imports: [ItemActionComponent], changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: "item.component.html", providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }], diff --git a/libs/components/src/item/item.mdx b/libs/components/src/item/item.mdx index ca697ebb4360..ab39e738b26b 100644 --- a/libs/components/src/item/item.mdx +++ b/libs/components/src/item/item.mdx @@ -111,6 +111,30 @@ Actions are commonly icon buttons or badge buttons. ``` +## Text Overflow Behavior + +The default behavior for long text is to truncate it. However, you have the option of changing it to +wrap instead if that is what the design calls for. + +This can be changed by passing `[truncate]="false"` to the `bit-item-content`. + +```html + + + Long text goes here! + This could also be very long text + + +``` + +### Truncation (Default) + + + +### Wrap + + + ## Item Groups Groups of items can be associated by wrapping them in the ``. diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index 5adf9d3c49d6..fd2d59c7ac23 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -53,6 +53,12 @@ export default { }), componentWrapperDecorator((story) => `
${story}
`), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-37011&t=b5tDKylm5sWm2yKo-11", + }, + }, } as Meta; type Story = StoryObj; @@ -129,7 +135,7 @@ export const ContentTypes: Story = { }), }; -export const TextOverflow: Story = { +export const TextOverflowTruncate: Story = { render: (args) => ({ props: args, template: /*html*/ ` @@ -152,6 +158,29 @@ export const TextOverflow: Story = { }), }; +export const TextOverflowWrap: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! + Worlddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd! + + + + + + + + + + + `, + }), +}; + const multipleActionListTemplate = /*html*/ ` diff --git a/libs/components/src/layout/layout.component.html b/libs/components/src/layout/layout.component.html index 7c1c5b2501d6..33b8de815721 100644 --- a/libs/components/src/layout/layout.component.html +++ b/libs/components/src/layout/layout.component.html @@ -23,19 +23,21 @@ -
+ }; + as data + ) {
-
+ class="tw-pointer-events-none tw-fixed tw-inset-0 tw-z-10 tw-bg-black tw-bg-opacity-0 motion-safe:tw-transition-colors md:tw-hidden" + [ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']" + > + @if (data.open) { +
+ } +
+ }

diff --git a/libs/components/src/layout/layout.stories.ts b/libs/components/src/layout/layout.stories.ts index 7fdad655548f..e09055df5965 100644 --- a/libs/components/src/layout/layout.stories.ts +++ b/libs/components/src/layout/layout.stories.ts @@ -31,6 +31,10 @@ export default { ], parameters: { chromatic: { viewports: [640, 1280] }, + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-51009&t=k6OTDDPZOTtypRqo-11", + }, }, } as Meta; diff --git a/libs/components/src/link/link.stories.ts b/libs/components/src/link/link.stories.ts index cc3d26dc9d29..d07d33ae5899 100644 --- a/libs/components/src/link/link.stories.ts +++ b/libs/components/src/link/link.stories.ts @@ -19,7 +19,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17419", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-39582&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/menu/menu.stories.ts b/libs/components/src/menu/menu.stories.ts index 65fafd2d04d7..f1f4d8df0006 100644 --- a/libs/components/src/menu/menu.stories.ts +++ b/libs/components/src/menu/menu.stories.ts @@ -17,7 +17,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17952", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40144&t=b5tDKylm5sWm2yKo-11", }, }, } as Meta; diff --git a/libs/components/src/multi-select/multi-select.component.html b/libs/components/src/multi-select/multi-select.component.html index 0c45e2d333fb..e157871e17ac 100644 --- a/libs/components/src/multi-select/multi-select.component.html +++ b/libs/components/src/multi-select/multi-select.component.html @@ -31,7 +31,9 @@ [disabled]="disabled" (click)="clear(item)" > - + @if (item.icon != null) { + + } {{ item.labelName }} @@ -41,10 +43,14 @@
- + @if (isSelected(item)) { + + }
- + @if (item.icon != null) { + + }
{{ item.listName }} diff --git a/libs/components/src/multi-select/multi-select.component.ts b/libs/components/src/multi-select/multi-select.component.ts index 53e51bfe2f91..cd92eb1d7ae8 100644 --- a/libs/components/src/multi-select/multi-select.component.ts +++ b/libs/components/src/multi-select/multi-select.component.ts @@ -2,7 +2,6 @@ // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { hasModifierKey } from "@angular/cdk/keycodes"; -import { NgIf } from "@angular/common"; import { Component, Input, @@ -24,10 +23,10 @@ import { import { NgSelectComponent, NgSelectModule } from "@ng-select/ng-select"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nPipe } from "@bitwarden/ui-common"; import { BadgeModule } from "../badge"; import { BitFormFieldControl } from "../form-field/form-field-control"; -import { I18nPipe } from "../shared/i18n.pipe"; import { SelectItemView } from "./models/select-item-view"; @@ -39,7 +38,7 @@ let nextId = 0; templateUrl: "./multi-select.component.html", providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }], standalone: true, - imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, NgIf, I18nPipe], + imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, I18nPipe], }) /** * This component has been implemented to only support Multi-select list events diff --git a/libs/components/src/navigation/nav-divider.component.html b/libs/components/src/navigation/nav-divider.component.html index 224f6ae0657d..64e43aeab4ea 100644 --- a/libs/components/src/navigation/nav-divider.component.html +++ b/libs/components/src/navigation/nav-divider.component.html @@ -1 +1,3 @@ -
+@if (sideNavService.open$ | async) { +
+} diff --git a/libs/components/src/navigation/nav-group.component.html b/libs/components/src/navigation/nav-group.component.html index 9f6d9ac034d3..9752fe56eb1f 100644 --- a/libs/components/src/navigation/nav-group.component.html +++ b/libs/components/src/navigation/nav-group.component.html @@ -1,5 +1,5 @@ - +@if (!hideIfEmpty || nestedNavComponents.length > 0) { - - - - + @if (variant === "tree") { + + + + } - + @if (variant !== "tree") { - + } - - -
- -
-
-
+ @if (sideNavService.open$ | async) { + @if (open) { +
+ +
+ } + } +} diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index d615bfe05827..62bdee26740a 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -12,8 +12,9 @@ import { SkipSelf, } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { IconButtonModule } from "../icon-button"; -import { I18nPipe } from "../shared/i18n.pipe"; import { NavBaseComponent } from "./nav-base.component"; import { NavGroupAbstraction, NavItemComponent } from "./nav-item.component"; @@ -28,6 +29,7 @@ import { SideNavService } from "./side-nav.service"; ], standalone: true, imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe], + preserveWhitespaces: false, }) export class NavGroupComponent extends NavBaseComponent implements AfterContentInit { @ContentChildren(NavBaseComponent, { diff --git a/libs/components/src/navigation/nav-group.stories.ts b/libs/components/src/navigation/nav-group.stories.ts index f412dbc20baf..6fbb89d4d9ed 100644 --- a/libs/components/src/navigation/nav-group.stories.ts +++ b/libs/components/src/navigation/nav-group.stories.ts @@ -62,7 +62,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4687%3A86642", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40145&t=b5tDKylm5sWm2yKo-4", }, chromatic: { viewports: [640, 1280] }, }, diff --git a/libs/components/src/navigation/nav-item.stories.ts b/libs/components/src/navigation/nav-item.stories.ts index 376f121eb003..0f6f406b2eb4 100644 --- a/libs/components/src/navigation/nav-item.stories.ts +++ b/libs/components/src/navigation/nav-item.stories.ts @@ -39,7 +39,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4687%3A86642", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40145&t=b5tDKylm5sWm2yKo-4", }, chromatic: { viewports: [640, 1280] }, }, diff --git a/libs/components/src/navigation/nav-logo.component.html b/libs/components/src/navigation/nav-logo.component.html index 427e926f2d74..a6169315333c 100644 --- a/libs/components/src/navigation/nav-logo.component.html +++ b/libs/components/src/navigation/nav-logo.component.html @@ -1,20 +1,23 @@ - - +@if (sideNavService.open) { +
+ + + +
+} +@if (!sideNavService.open) { + +} diff --git a/libs/components/src/navigation/nav-logo.component.ts b/libs/components/src/navigation/nav-logo.component.ts index 8a84970500cf..de9d801e5533 100644 --- a/libs/components/src/navigation/nav-logo.component.ts +++ b/libs/components/src/navigation/nav-logo.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf } from "@angular/common"; + import { Component, Input } from "@angular/core"; import { RouterLinkActive, RouterLink } from "@angular/router"; @@ -14,7 +14,7 @@ import { SideNavService } from "./side-nav.service"; selector: "bit-nav-logo", templateUrl: "./nav-logo.component.html", standalone: true, - imports: [NgIf, RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent], + imports: [RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent], }) export class NavLogoComponent { /** Icon that is displayed when the side nav is closed */ diff --git a/libs/components/src/navigation/side-nav.component.html b/libs/components/src/navigation/side-nav.component.html index 05c99c7d64e8..3b77c981be46 100644 --- a/libs/components/src/navigation/side-nav.component.html +++ b/libs/components/src/navigation/side-nav.component.html @@ -1,42 +1,46 @@ - + +} diff --git a/libs/components/src/navigation/side-nav.component.ts b/libs/components/src/navigation/side-nav.component.ts index c86a517100f6..e8e4f131d6d0 100644 --- a/libs/components/src/navigation/side-nav.component.ts +++ b/libs/components/src/navigation/side-nav.component.ts @@ -4,8 +4,9 @@ import { CdkTrapFocus } from "@angular/cdk/a11y"; import { CommonModule } from "@angular/common"; import { Component, ElementRef, Input, ViewChild } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitIconButtonComponent } from "../icon-button/icon-button.component"; -import { I18nPipe } from "../shared/i18n.pipe"; import { NavDividerComponent } from "./nav-divider.component"; import { SideNavService } from "./side-nav.service"; diff --git a/libs/components/src/no-items/no-items.stories.ts b/libs/components/src/no-items/no-items.stories.ts index d8e5b59bdbff..48d52476b174 100644 --- a/libs/components/src/no-items/no-items.stories.ts +++ b/libs/components/src/no-items/no-items.stories.ts @@ -13,6 +13,12 @@ export default { imports: [ButtonModule, NoItemsModule], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21665-25102&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/popover/popover.stories.ts b/libs/components/src/popover/popover.stories.ts index 10b7b248b794..fca4d6596079 100644 --- a/libs/components/src/popover/popover.stories.ts +++ b/libs/components/src/popover/popover.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1717-15868", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40852&t=b5tDKylm5sWm2yKo-4", }, }, argTypes: { diff --git a/libs/components/src/progress/progress.component.html b/libs/components/src/progress/progress.component.html index 2637f23eee8a..30b68d9d6457 100644 --- a/libs/components/src/progress/progress.component.html +++ b/libs/components/src/progress/progress.component.html @@ -7,13 +7,12 @@ attr.aria-valuenow="{{ barWidth }}" [ngStyle]="{ width: barWidth + '%' }" > -
- -
 
-
{{ textContent }}
-
+ @if (displayText) { +
+ +
 
+
{{ textContent }}
+
+ }
diff --git a/libs/components/src/progress/progress.stories.ts b/libs/components/src/progress/progress.stories.ts index 49a5398d2d9a..1484dab0a212 100644 --- a/libs/components/src/progress/progress.stories.ts +++ b/libs/components/src/progress/progress.stories.ts @@ -8,7 +8,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A18185&t=AM0acaIJ00BUhZKz-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40933&t=b5tDKylm5sWm2yKo-4", }, }, args: { diff --git a/libs/components/src/radio-button/radio-button.stories.ts b/libs/components/src/radio-button/radio-button.stories.ts index dcef13a19be0..f5d7f6732f55 100644 --- a/libs/components/src/radio-button/radio-button.stories.ts +++ b/libs/components/src/radio-button/radio-button.stories.ts @@ -31,7 +31,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-35836&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/radio-button/radio-group.component.html b/libs/components/src/radio-button/radio-group.component.html index 128a723d4613..b71abd9249c5 100644 --- a/libs/components/src/radio-button/radio-group.component.html +++ b/libs/components/src/radio-button/radio-group.component.html @@ -1,16 +1,18 @@ - +@if (label) {
- ({{ "required" | i18n }}) + @if (required) { + ({{ "required" | i18n }}) + }
-
+} - +@if (!label) { - +}
diff --git a/libs/components/src/radio-button/radio-group.component.ts b/libs/components/src/radio-button/radio-group.component.ts index b9e48f464454..895f769af508 100644 --- a/libs/components/src/radio-button/radio-group.component.ts +++ b/libs/components/src/radio-button/radio-group.component.ts @@ -1,11 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf, NgTemplateOutlet } from "@angular/common"; +import { NgTemplateOutlet } from "@angular/common"; import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core"; import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitLabel } from "../form-control/label.component"; -import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; @@ -13,7 +14,7 @@ let nextId = 0; selector: "bit-radio-group", templateUrl: "radio-group.component.html", standalone: true, - imports: [NgIf, NgTemplateOutlet, I18nPipe], + imports: [NgTemplateOutlet, I18nPipe], }) export class RadioGroupComponent implements ControlValueAccessor { selected: unknown; diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index 9a811ce6777e..7f1bd781e9dd 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -9,10 +9,10 @@ import { } from "@angular/forms"; import { isBrowserSafariApi } from "@bitwarden/platform"; +import { I18nPipe } from "@bitwarden/ui-common"; import { InputModule } from "../input/input.module"; import { FocusableElement } from "../shared/focusable-element"; -import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; diff --git a/libs/components/src/section/section.stories.ts b/libs/components/src/section/section.stories.ts index 0d36d6e5a113..53e6bc078c5d 100644 --- a/libs/components/src/section/section.stories.ts +++ b/libs/components/src/section/section.stories.ts @@ -22,6 +22,12 @@ export default { }), componentWrapperDecorator((story) => `
${story}
`), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21666-19363&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/select/select.component.html b/libs/components/src/select/select.component.html index 848692526a14..dcca7ae195e5 100644 --- a/libs/components/src/select/select.component.html +++ b/libs/components/src/select/select.component.html @@ -13,7 +13,9 @@
- + @if (item.icon != null) { + + }
{{ item.label }} diff --git a/libs/components/src/select/select.component.ts b/libs/components/src/select/select.component.ts index 8f75c5be42b1..a89eb87f54b2 100644 --- a/libs/components/src/select/select.component.ts +++ b/libs/components/src/select/select.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf } from "@angular/common"; + import { Component, ContentChildren, @@ -36,7 +36,7 @@ let nextId = 0; templateUrl: "select.component.html", providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }], standalone: true, - imports: [NgSelectModule, ReactiveFormsModule, FormsModule, NgIf], + imports: [NgSelectModule, ReactiveFormsModule, FormsModule], }) export class SelectComponent implements BitFormFieldControl, ControlValueAccessor { @ViewChild(NgSelectComponent) select: NgSelectComponent; diff --git a/libs/components/src/select/select.stories.ts b/libs/components/src/select/select.stories.ts index 4bc85d8dba20..c030bea86c5d 100644 --- a/libs/components/src/select/select.stories.ts +++ b/libs/components/src/select/select.stories.ts @@ -33,7 +33,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/3tWtMSYoLB0ZLEimLNzYsm/End-user-%26-admin-Vault-Refresh?t=7QEmGA69YTOF8sXU-0", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/shared/shared.module.ts b/libs/components/src/shared/shared.module.ts index 253b049f8feb..99d052c33508 100644 --- a/libs/components/src/shared/shared.module.ts +++ b/libs/components/src/shared/shared.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { I18nPipe } from "./i18n.pipe"; +import { I18nPipe } from "@bitwarden/ui-common"; @NgModule({ imports: [CommonModule, I18nPipe], diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts index c63b36ea89c4..5fc01d37d533 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts @@ -49,11 +49,9 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Your favorite color - + @for (color of colors; track color) { + + } diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index 13f0a16a4d74..9c609300ed10 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -15,8 +15,8 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; Dialog body text goes here. - - + + `, @@ -36,13 +36,15 @@ class KitchenSinkDialog {

- - {{ item.name }} - + @for (item of navItems; track item) { + + {{ item.name }} + + }

-
+

Bitwarden Kitchen Sink

Learn more
@@ -68,8 +70,8 @@ class KitchenSinkDialog {

About

- - + +

Companies using Bitwarden

diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts index 3c6d6f114449..ba71483d7de4 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts @@ -20,7 +20,11 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Password Manager Everyone - + Anchor link Another link @@ -33,7 +37,11 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Secrets Manager Developers - + Anchor link Another link diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts index 2804c9e83511..c71140d8166d 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts @@ -7,7 +7,7 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; selector: "bit-kitchen-sink-toggle-list", imports: [KitchenSinkSharedModule], template: ` -
+
All 3 @@ -16,11 +16,15 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Mid-sized 1
-
    -
  • - {{ company.name }} -
  • -
+ @for (company of companyList; track company) { +
    + @if (company.size === selectedToggle || selectedToggle === "all") { +
  • + {{ company.name }} +
  • + } +
+ } `, }) export class KitchenSinkToggleList { diff --git a/libs/components/src/table/sortable.component.ts b/libs/components/src/table/sortable.component.ts index d3309c03aa91..bdfb87ac52f3 100644 --- a/libs/components/src/table/sortable.component.ts +++ b/libs/components/src/table/sortable.component.ts @@ -10,7 +10,12 @@ import { TableComponent } from "./table.component"; @Component({ selector: "th[bitSortable]", template: ` - diff --git a/libs/components/src/table/table.stories.ts b/libs/components/src/table/table.stories.ts index 4ebc3045d135..e8ab24ee8b7e 100644 --- a/libs/components/src/table/table.stories.ts +++ b/libs/components/src/table/table.stories.ts @@ -21,7 +21,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A18371", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-41282&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/tabs/shared/tab-list-item.directive.ts b/libs/components/src/tabs/shared/tab-list-item.directive.ts index 87435133a23f..2a71a385a835 100644 --- a/libs/components/src/tabs/shared/tab-list-item.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-item.directive.ts @@ -44,7 +44,7 @@ export class TabListItemDirective implements FocusableOption { */ get textColorClassList(): string[] { if (this.disabled) { - return ["!tw-text-muted", "hover:!tw-text-muted"]; + return ["!tw-text-secondary-300", "hover:!tw-text-secondary-300"]; } if (this.active) { return ["!tw-text-primary-600", "hover:!tw-text-primary-700"]; @@ -60,7 +60,7 @@ export class TabListItemDirective implements FocusableOption { "tw-px-4", "tw-font-semibold", "tw-transition", - "tw-rounded-t", + "tw-rounded-t-lg", "tw-border-0", "tw-border-x", "tw-border-t-4", @@ -71,12 +71,12 @@ export class TabListItemDirective implements FocusableOption { "focus-visible:tw-z-10", "focus-visible:tw-outline-none", "focus-visible:tw-ring-2", - "focus-visible:tw-ring-primary-700", + "focus-visible:tw-ring-primary-600", ]; } get disabledClassList(): string[] { - return ["!tw-bg-secondary-100", "!tw-no-underline", "tw-cursor-not-allowed"]; + return ["!tw-no-underline", "tw-cursor-not-allowed"]; } get activeClassList(): string[] { @@ -87,6 +87,7 @@ export class TabListItemDirective implements FocusableOption { "tw-border-b", "tw-border-b-background", "!tw-bg-background", + "hover:tw-no-underline", "hover:tw-border-t-primary-700", "focus-visible:tw-border-t-primary-700", "focus-visible:!tw-text-primary-700", diff --git a/libs/components/src/tabs/tab-group/tab-group.component.html b/libs/components/src/tabs/tab-group/tab-group.component.html index 071f5c2259f6..52fa193de964 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.html +++ b/libs/components/src/tabs/tab-group/tab-group.component.html @@ -5,40 +5,41 @@ [attr.aria-label]="label" (keydown)="keyManager.onKeydown($event)" > - + + }
- - + @for (tab of tabs; track tab; let i = $index) { + + + }
diff --git a/libs/components/src/tabs/tab-group/tab-group.component.ts b/libs/components/src/tabs/tab-group/tab-group.component.ts index 54d00343b384..b525b9b6723e 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.ts +++ b/libs/components/src/tabs/tab-group/tab-group.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { FocusKeyManager } from "@angular/cdk/a11y"; import { coerceNumberProperty } from "@angular/cdk/coercion"; -import { CommonModule } from "@angular/common"; +import { NgTemplateOutlet } from "@angular/common"; import { AfterContentChecked, AfterContentInit, @@ -33,7 +33,7 @@ let nextId = 0; templateUrl: "./tab-group.component.html", standalone: true, imports: [ - CommonModule, + NgTemplateOutlet, TabHeaderComponent, TabListContainerDirective, TabListItemDirective, diff --git a/libs/components/src/tabs/tabs.stories.ts b/libs/components/src/tabs/tabs.stories.ts index 6b460d8ee001..250a74430650 100644 --- a/libs/components/src/tabs/tabs.stories.ts +++ b/libs/components/src/tabs/tabs.stories.ts @@ -74,7 +74,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17922", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-41432&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/toast/toast.component.html b/libs/components/src/toast/toast.component.html index 84154cba6119..d78cc7783aa6 100644 --- a/libs/components/src/toast/toast.component.html +++ b/libs/components/src/toast/toast.component.html @@ -7,15 +7,14 @@
{{ variant | i18n }} -

{{ title }}

-

- {{ m }} -

+ @if (title) { +

{{ title }}

+ } + @for (m of messageArray; track m) { +

+ {{ m }} +

+ }
diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index f2bf7471d44f..5b59055bc4e8 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -23,13 +23,17 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ClientType } from "@bitwarden/common/enums"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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"; @@ -138,8 +142,14 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { @Input() set organizationId(value: string) { this._organizationId = value; - this.organizationService - .get$(this._organizationId) + getUserId(this.accountService.activeAccount$) + .pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this._organizationId)), + ), + ) .pipe(takeUntil(this.destroy$)) .subscribe((organization) => { this._organizationId = organization?.id; @@ -294,8 +304,9 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { } private async initializeOrganizations() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizations$ = concat( - this.organizationService.memberOrganizations$.pipe( + this.organizationService.memberOrganizations$(userId).pipe( // Import is an alternative way to create collections during onboarding, so import from Password Manager // is available to any user who can create collections in the organization. map((orgs) => orgs.filter((org) => org.canAccessImport || org.canCreateNewCollections)), @@ -408,7 +419,14 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { if (!organizationId) { return false; } - return (await this.organizationService.get(this.organizationId))?.canAccessImport; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + return ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ) + )?.canAccessImport; } getFormatInstructionTitle() { diff --git a/libs/importer/spec/base-importer.spec.ts b/libs/importer/src/importers/base-importer.spec.ts similarity index 98% rename from libs/importer/spec/base-importer.spec.ts rename to libs/importer/src/importers/base-importer.spec.ts index f72ceccb538b..309bb7ca8c49 100644 --- a/libs/importer/spec/base-importer.spec.ts +++ b/libs/importer/src/importers/base-importer.spec.ts @@ -2,7 +2,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { BaseImporter } from "../src/importers/base-importer"; +import { BaseImporter } from "./base-importer"; class FakeBaseImporter extends BaseImporter { initLoginCipher(): CipherView { diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index ebf297cd0e63..f01e6571439b 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -5,12 +5,12 @@ import { firstValueFrom, map } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherWithIdExport, CollectionWithIdExport, FolderWithIdExport, } from "@bitwarden/common/models/export"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; diff --git a/libs/importer/spec/bitwarden-password-protected-importer.spec.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts similarity index 91% rename from libs/importer/spec/bitwarden-password-protected-importer.spec.ts rename to libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts index 008d6e25f92f..66deabf0634e 100644 --- a/libs/importer/spec/bitwarden-password-protected-importer.spec.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts @@ -2,19 +2,17 @@ import { mock, MockProxy } from "jest-mock-extended"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { KdfType, KeyService } from "@bitwarden/key-management"; -import { - BitwardenPasswordProtectedImporter, - BitwardenJsonImporter, -} from "../src/importers/bitwarden"; +import { emptyAccountEncrypted } from "../spec-data/bitwarden-json/account-encrypted.json"; +import { emptyUnencryptedExport } from "../spec-data/bitwarden-json/unencrypted.json"; -import { emptyAccountEncrypted } from "./test-data/bitwarden-json/account-encrypted.json"; -import { emptyUnencryptedExport } from "./test-data/bitwarden-json/unencrypted.json"; +import { BitwardenJsonImporter } from "./bitwarden-json-importer"; +import { BitwardenPasswordProtectedImporter } from "./bitwarden-password-protected-importer"; describe("BitwardenPasswordProtectedImporter", () => { let importer: BitwardenPasswordProtectedImporter; diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts index 10b02426af0d..02a417c21691 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; diff --git a/libs/importer/spec/chrome-csv-importer.spec.ts b/libs/importer/src/importers/chrome-csv-importer.spec.ts similarity index 92% rename from libs/importer/spec/chrome-csv-importer.spec.ts rename to libs/importer/src/importers/chrome-csv-importer.spec.ts index dcb39b657d59..d0e82cf44ac6 100644 --- a/libs/importer/spec/chrome-csv-importer.spec.ts +++ b/libs/importer/src/importers/chrome-csv-importer.spec.ts @@ -2,10 +2,9 @@ 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 { ChromeCsvImporter } from "../src/importers"; - -import { data as androidData } from "./test-data/chrome-csv/android-data.csv"; -import { data as simplePasswordData } from "./test-data/chrome-csv/simple-password-data.csv"; +import { ChromeCsvImporter } from "./chrome-csv-importer"; +import { data as androidData } from "./spec-data/chrome-csv/android-data.csv"; +import { data as simplePasswordData } from "./spec-data/chrome-csv/simple-password-data.csv"; const CipherData = [ { diff --git a/libs/importer/spec/dashlane-csv-importer.spec.ts b/libs/importer/src/importers/dashlane/dashlane-csv-importer.spec.ts similarity index 96% rename from libs/importer/spec/dashlane-csv-importer.spec.ts rename to libs/importer/src/importers/dashlane/dashlane-csv-importer.spec.ts index 1d76396022cd..b8d84a9378ae 100644 --- a/libs/importer/spec/dashlane-csv-importer.spec.ts +++ b/libs/importer/src/importers/dashlane/dashlane-csv-importer.spec.ts @@ -1,14 +1,13 @@ import { CipherType } from "@bitwarden/common/vault/enums"; -import { DashlaneCsvImporter } from "../src/importers"; - -import { credentialsData_otpUrl } from "./test-data/dashlane-csv/credentials-otpurl.csv"; -import { credentialsData } from "./test-data/dashlane-csv/credentials.csv"; -import { identityData } from "./test-data/dashlane-csv/id.csv"; -import { multiplePersonalInfoData } from "./test-data/dashlane-csv/multiple-personal-info.csv"; -import { paymentsData } from "./test-data/dashlane-csv/payments.csv"; -import { personalInfoData } from "./test-data/dashlane-csv/personal-info.csv"; -import { secureNoteData } from "./test-data/dashlane-csv/securenotes.csv"; +import { DashlaneCsvImporter } from ".."; +import { credentialsData_otpUrl } from "../spec-data/dashlane-csv/credentials-otpurl.csv"; +import { credentialsData } from "../spec-data/dashlane-csv/credentials.csv"; +import { identityData } from "../spec-data/dashlane-csv/id.csv"; +import { multiplePersonalInfoData } from "../spec-data/dashlane-csv/multiple-personal-info.csv"; +import { paymentsData } from "../spec-data/dashlane-csv/payments.csv"; +import { personalInfoData } from "../spec-data/dashlane-csv/personal-info.csv"; +import { secureNoteData } from "../spec-data/dashlane-csv/securenotes.csv"; describe("Dashlane CSV Importer", () => { let importer: DashlaneCsvImporter; diff --git a/libs/importer/spec/enpass-json-importer.spec.ts b/libs/importer/src/importers/enpass/enpass-json-importer.spec.ts similarity index 93% rename from libs/importer/spec/enpass-json-importer.spec.ts rename to libs/importer/src/importers/enpass/enpass-json-importer.spec.ts index 75e668f28be1..b0cb5f71524c 100644 --- a/libs/importer/spec/enpass-json-importer.spec.ts +++ b/libs/importer/src/importers/enpass/enpass-json-importer.spec.ts @@ -1,13 +1,13 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; -import { EnpassJsonImporter } from "../src/importers"; +import { creditCard } from "../spec-data/enpass-json/credit-card"; +import { folders } from "../spec-data/enpass-json/folders"; +import { login } from "../spec-data/enpass-json/login"; +import { loginAndroidUrl } from "../spec-data/enpass-json/login-android-url"; +import { note } from "../spec-data/enpass-json/note"; -import { creditCard } from "./test-data/enpass-json/credit-card"; -import { folders } from "./test-data/enpass-json/folders"; -import { login } from "./test-data/enpass-json/login"; -import { loginAndroidUrl } from "./test-data/enpass-json/login-android-url"; -import { note } from "./test-data/enpass-json/note"; +import { EnpassJsonImporter } from "./enpass-json-importer"; function validateCustomField(fields: FieldView[], fieldName: string, expectedValue: any) { expect(fields).toBeDefined(); diff --git a/libs/importer/spec/firefox-csv-importer.spec.ts b/libs/importer/src/importers/firefox-csv-importer.spec.ts similarity index 91% rename from libs/importer/spec/firefox-csv-importer.spec.ts rename to libs/importer/src/importers/firefox-csv-importer.spec.ts index 0add655a6849..78bca0599b57 100644 --- a/libs/importer/spec/firefox-csv-importer.spec.ts +++ b/libs/importer/src/importers/firefox-csv-importer.spec.ts @@ -2,10 +2,9 @@ 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 { FirefoxCsvImporter } from "../src/importers"; - -import { data as firefoxAccountsData } from "./test-data/firefox-csv/firefox-accounts-data.csv"; -import { data as simplePasswordData } from "./test-data/firefox-csv/simple-password-data.csv"; +import { FirefoxCsvImporter } from "./firefox-csv-importer"; +import { data as firefoxAccountsData } from "./spec-data/firefox-csv/firefox-accounts-data.csv"; +import { data as simplePasswordData } from "./spec-data/firefox-csv/simple-password-data.csv"; const CipherData = [ { diff --git a/libs/importer/src/importers/index.ts b/libs/importer/src/importers/index.ts index 1ba3a0d9eb82..c9b4c4c9c19b 100644 --- a/libs/importer/src/importers/index.ts +++ b/libs/importer/src/importers/index.ts @@ -22,7 +22,7 @@ export { KasperskyTxtImporter } from "./kaspersky-txt-importer"; export { KeePass2XmlImporter } from "./keepass2-xml-importer"; export { KeePassXCsvImporter } from "./keepassx-csv-importer"; export { KeeperCsvImporter, KeeperJsonImporter } from "./keeper"; -export { LastPassCsvImporter } from "./lastpass-csv-importer"; +export { LastPassCsvImporter } from "./lastpass"; export { LogMeOnceCsvImporter } from "./logmeonce-csv-importer"; export { MeldiumCsvImporter } from "./meldium-csv-importer"; export { MSecureCsvImporter } from "./msecure-csv-importer"; diff --git a/libs/importer/spec/keepass2-xml-importer.spec.ts b/libs/importer/src/importers/keepass2-xml-importer.spec.ts similarity index 92% rename from libs/importer/spec/keepass2-xml-importer.spec.ts rename to libs/importer/src/importers/keepass2-xml-importer.spec.ts index 4f02bf9260b0..8fbb021883ca 100644 --- a/libs/importer/spec/keepass2-xml-importer.spec.ts +++ b/libs/importer/src/importers/keepass2-xml-importer.spec.ts @@ -1,12 +1,11 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { KeePass2XmlImporter } from "../src/importers"; - +import { KeePass2XmlImporter } from "./keepass2-xml-importer"; import { TestData, TestData1, TestData2, -} from "./test-data/keepass2-xml/keepass2-xml-importer-testdata"; +} from "./spec-data/keepass2-xml/keepass2-xml-importer-testdata"; describe("KeePass2 Xml Importer", () => { it("should parse XML data", async () => { diff --git a/libs/importer/spec/keepassx-csv-importer.spec.ts b/libs/importer/src/importers/keepassx-csv-importer.spec.ts similarity index 92% rename from libs/importer/spec/keepassx-csv-importer.spec.ts rename to libs/importer/src/importers/keepassx-csv-importer.spec.ts index 0b3d729d9ded..e0b8a6cff5d0 100644 --- a/libs/importer/spec/keepassx-csv-importer.spec.ts +++ b/libs/importer/src/importers/keepassx-csv-importer.spec.ts @@ -1,6 +1,5 @@ -import { KeePassXCsvImporter } from "../src/importers"; - -import { keepassxTestData } from "./test-data/keepassx-csv/testdata.csv"; +import { KeePassXCsvImporter } from "./keepassx-csv-importer"; +import { keepassxTestData } from "./spec-data/keepassx-csv/testdata.csv"; describe("KeePassX CSV Importer", () => { let importer: KeePassXCsvImporter; diff --git a/libs/importer/spec/keeper-csv-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts similarity index 97% rename from libs/importer/spec/keeper-csv-importer.spec.ts rename to libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts index f466a766ccb0..69655eb91779 100644 --- a/libs/importer/spec/keeper-csv-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts @@ -1,8 +1,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { KeeperCsvImporter } from "../src/importers"; +import { testData as TestData } from "../spec-data/keeper-csv/testdata.csv"; -import { testData as TestData } from "./test-data/keeper-csv/testdata.csv"; +import { KeeperCsvImporter } from "./keeper-csv-importer"; describe("Keeper CSV Importer", () => { let importer: KeeperCsvImporter; diff --git a/libs/importer/spec/keeper-json-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts similarity index 96% rename from libs/importer/spec/keeper-json-importer.spec.ts rename to libs/importer/src/importers/keeper/keeper-json-importer.spec.ts index 37dfadc304b7..31169021e0c4 100644 --- a/libs/importer/spec/keeper-json-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts @@ -1,8 +1,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { KeeperJsonImporter } from "../src/importers"; +import { testData as TestData } from "../spec-data/keeper-json/testdata.json"; -import { testData as TestData } from "./test-data/keeper-json/testdata.json"; +import { KeeperJsonImporter } from "./keeper-json-importer"; describe("Keeper Json Importer", () => { const testDataJson = JSON.stringify(TestData); diff --git a/libs/importer/src/importers/lastpass/index.ts b/libs/importer/src/importers/lastpass/index.ts new file mode 100644 index 000000000000..0949fdea1b62 --- /dev/null +++ b/libs/importer/src/importers/lastpass/index.ts @@ -0,0 +1 @@ +export { LastPassCsvImporter } from "./lastpass-csv-importer"; diff --git a/libs/importer/spec/lastpass-csv-importer.spec.ts b/libs/importer/src/importers/lastpass/lastpass-csv-importer.spec.ts similarity index 97% rename from libs/importer/spec/lastpass-csv-importer.spec.ts rename to libs/importer/src/importers/lastpass/lastpass-csv-importer.spec.ts index de43c4aa473f..cabd246fa7e8 100644 --- a/libs/importer/spec/lastpass-csv-importer.spec.ts +++ b/libs/importer/src/importers/lastpass/lastpass-csv-importer.spec.ts @@ -2,8 +2,9 @@ import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; -import { LastPassCsvImporter } from "../src/importers"; -import { ImportResult } from "../src/models/import-result"; +import { ImportResult } from "../../models/import-result"; + +import { LastPassCsvImporter } from "./lastpass-csv-importer"; function baseExcept(result: ImportResult) { expect(result).not.toBeNull(); diff --git a/libs/importer/src/importers/lastpass-csv-importer.ts b/libs/importer/src/importers/lastpass/lastpass-csv-importer.ts similarity index 98% rename from libs/importer/src/importers/lastpass-csv-importer.ts rename to libs/importer/src/importers/lastpass/lastpass-csv-importer.ts index 50a5b46239d2..8f239ec68306 100644 --- a/libs/importer/src/importers/lastpass-csv-importer.ts +++ b/libs/importer/src/importers/lastpass/lastpass-csv-importer.ts @@ -8,10 +8,9 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view" import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; -import { ImportResult } from "../models/import-result"; - -import { BaseImporter } from "./base-importer"; -import { Importer } from "./importer"; +import { ImportResult } from "../../models/import-result"; +import { BaseImporter } from "../base-importer"; +import { Importer } from "../importer"; export class LastPassCsvImporter extends BaseImporter implements Importer { parse(data: string): Promise { diff --git a/libs/importer/spec/msecure-csv-importer.spec.ts b/libs/importer/src/importers/msecure-csv-importer.spec.ts similarity index 98% rename from libs/importer/spec/msecure-csv-importer.spec.ts rename to libs/importer/src/importers/msecure-csv-importer.spec.ts index 903be3bcb181..83e35802facb 100644 --- a/libs/importer/spec/msecure-csv-importer.spec.ts +++ b/libs/importer/src/importers/msecure-csv-importer.spec.ts @@ -1,6 +1,6 @@ import { CipherType } from "@bitwarden/common/vault/enums"; -import { MSecureCsvImporter } from "../src/importers/msecure-csv-importer"; +import { MSecureCsvImporter } from "./msecure-csv-importer"; describe("MSecureCsvImporter.parse", () => { let importer: MSecureCsvImporter; diff --git a/libs/importer/spec/myki-csv-importer.spec.ts b/libs/importer/src/importers/myki-csv-importer.spec.ts similarity index 98% rename from libs/importer/spec/myki-csv-importer.spec.ts rename to libs/importer/src/importers/myki-csv-importer.spec.ts index e1e36478a6a9..6f804523ef08 100644 --- a/libs/importer/spec/myki-csv-importer.spec.ts +++ b/libs/importer/src/importers/myki-csv-importer.spec.ts @@ -1,14 +1,13 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { MykiCsvImporter } from "../src/importers"; - -import { userAccountData } from "./test-data/myki-csv/user-account.csv"; -import { userCreditCardData } from "./test-data/myki-csv/user-credit-card.csv"; -import { userIdCardData } from "./test-data/myki-csv/user-id-card.csv"; -import { userIdentityData } from "./test-data/myki-csv/user-identity.csv"; -import { userNoteData } from "./test-data/myki-csv/user-note.csv"; -import { userTwoFaData } from "./test-data/myki-csv/user-twofa.csv"; +import { MykiCsvImporter } from "./myki-csv-importer"; +import { userAccountData } from "./spec-data/myki-csv/user-account.csv"; +import { userCreditCardData } from "./spec-data/myki-csv/user-credit-card.csv"; +import { userIdCardData } from "./spec-data/myki-csv/user-id-card.csv"; +import { userIdentityData } from "./spec-data/myki-csv/user-identity.csv"; +import { userNoteData } from "./spec-data/myki-csv/user-note.csv"; +import { userTwoFaData } from "./spec-data/myki-csv/user-twofa.csv"; function expectDriversLicense(cipher: CipherView) { expect(cipher.type).toBe(CipherType.Identity); diff --git a/libs/importer/spec/netwrix-passwordsecure-csv-importer.spec.ts b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts similarity index 95% rename from libs/importer/spec/netwrix-passwordsecure-csv-importer.spec.ts rename to libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts index ab893dbc56cf..ff327daf04d6 100644 --- a/libs/importer/spec/netwrix-passwordsecure-csv-importer.spec.ts +++ b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts @@ -1,8 +1,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { NetwrixPasswordSecureCsvImporter } from "../src/importers"; +import { credentialsData } from "../spec-data/netwrix-csv/login-export.csv"; -import { credentialsData } from "./test-data/netwrix-csv/login-export.csv"; +import { NetwrixPasswordSecureCsvImporter } from "./netwrix-passwordsecure-csv-importer"; describe("Netwrix Password Secure CSV Importer", () => { let importer: NetwrixPasswordSecureCsvImporter; diff --git a/libs/importer/spec/nordpass-csv-importer.spec.ts b/libs/importer/src/importers/nordpass-csv-importer.spec.ts similarity index 95% rename from libs/importer/spec/nordpass-csv-importer.spec.ts rename to libs/importer/src/importers/nordpass-csv-importer.spec.ts index a7bbf0fc79c2..cadc7bca28c6 100644 --- a/libs/importer/spec/nordpass-csv-importer.spec.ts +++ b/libs/importer/src/importers/nordpass-csv-importer.spec.ts @@ -3,13 +3,12 @@ import { SecureNoteType, CipherType, FieldType } from "@bitwarden/common/vault/e import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; -import { NordPassCsvImporter } from "../src/importers"; - -import { data as creditCardData } from "./test-data/nordpass-csv/nordpass.card.csv"; -import { data as identityData } from "./test-data/nordpass-csv/nordpass.identity.csv"; -import { data as loginDataWithAdditionalUrls } from "./test-data/nordpass-csv/nordpass.login-with-additinal-urls.csv"; -import { data as loginData } from "./test-data/nordpass-csv/nordpass.login.csv"; -import { data as secureNoteData } from "./test-data/nordpass-csv/nordpass.secure-note.csv"; +import { NordPassCsvImporter } from "./nordpass-csv-importer"; +import { data as creditCardData } from "./spec-data/nordpass-csv/nordpass.card.csv"; +import { data as identityData } from "./spec-data/nordpass-csv/nordpass.identity.csv"; +import { data as loginDataWithAdditionalUrls } from "./spec-data/nordpass-csv/nordpass.login-with-additinal-urls.csv"; +import { data as loginData } from "./spec-data/nordpass-csv/nordpass.login.csv"; +import { data as secureNoteData } from "./spec-data/nordpass-csv/nordpass.secure-note.csv"; const namesTestData = [ { diff --git a/libs/importer/spec/onepassword-1pif-importer.spec.ts b/libs/importer/src/importers/onepassword/onepassword-1pif-importer.spec.ts similarity index 99% rename from libs/importer/spec/onepassword-1pif-importer.spec.ts rename to libs/importer/src/importers/onepassword/onepassword-1pif-importer.spec.ts index fbf79ec8e0b1..deaf2c904b24 100644 --- a/libs/importer/spec/onepassword-1pif-importer.spec.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pif-importer.spec.ts @@ -1,6 +1,6 @@ import { FieldType } from "@bitwarden/common/vault/enums"; -import { OnePassword1PifImporter } from "../src/importers"; +import { OnePassword1PifImporter } from "./onepassword-1pif-importer"; const TestData: string = "***aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee***\n" + diff --git a/libs/importer/spec/onepassword-1pux-importer.spec.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts similarity index 93% rename from libs/importer/spec/onepassword-1pux-importer.spec.ts rename to libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts index 93de4be606bd..d4976f7a1982 100644 --- a/libs/importer/spec/onepassword-1pux-importer.spec.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts @@ -2,32 +2,32 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; -import { OnePassword1PuxImporter } from "../src/importers"; - -import { APICredentialsData } from "./test-data/onepassword-1pux/api-credentials"; -import { BankAccountData } from "./test-data/onepassword-1pux/bank-account"; -import { CreditCardData } from "./test-data/onepassword-1pux/credit-card"; -import { DatabaseData } from "./test-data/onepassword-1pux/database"; -import { DriversLicenseData } from "./test-data/onepassword-1pux/drivers-license"; -import { EmailAccountData } from "./test-data/onepassword-1pux/email-account"; -import { EmailFieldData } from "./test-data/onepassword-1pux/email-field"; -import { EmailFieldOnIdentityData } from "./test-data/onepassword-1pux/email-field-on-identity"; -import { EmailFieldOnIdentityPrefilledData } from "./test-data/onepassword-1pux/email-field-on-identity_prefilled"; -import { IdentityData } from "./test-data/onepassword-1pux/identity-data"; -import { LoginData } from "./test-data/onepassword-1pux/login-data"; -import { MedicalRecordData } from "./test-data/onepassword-1pux/medical-record"; -import { MembershipData } from "./test-data/onepassword-1pux/membership"; -import { OnePuxExampleFile } from "./test-data/onepassword-1pux/onepux_example"; -import { OutdoorLicenseData } from "./test-data/onepassword-1pux/outdoor-license"; -import { PassportData } from "./test-data/onepassword-1pux/passport"; -import { PasswordData } from "./test-data/onepassword-1pux/password"; -import { RewardsProgramData } from "./test-data/onepassword-1pux/rewards-program"; -import { SanitizedExport } from "./test-data/onepassword-1pux/sanitized-export"; -import { SecureNoteData } from "./test-data/onepassword-1pux/secure-note"; -import { ServerData } from "./test-data/onepassword-1pux/server"; -import { SoftwareLicenseData } from "./test-data/onepassword-1pux/software-license"; -import { SSNData } from "./test-data/onepassword-1pux/ssn"; -import { WirelessRouterData } from "./test-data/onepassword-1pux/wireless-router"; +import { APICredentialsData } from "../spec-data/onepassword-1pux/api-credentials"; +import { BankAccountData } from "../spec-data/onepassword-1pux/bank-account"; +import { CreditCardData } from "../spec-data/onepassword-1pux/credit-card"; +import { DatabaseData } from "../spec-data/onepassword-1pux/database"; +import { DriversLicenseData } from "../spec-data/onepassword-1pux/drivers-license"; +import { EmailAccountData } from "../spec-data/onepassword-1pux/email-account"; +import { EmailFieldData } from "../spec-data/onepassword-1pux/email-field"; +import { EmailFieldOnIdentityData } from "../spec-data/onepassword-1pux/email-field-on-identity"; +import { EmailFieldOnIdentityPrefilledData } from "../spec-data/onepassword-1pux/email-field-on-identity_prefilled"; +import { IdentityData } from "../spec-data/onepassword-1pux/identity-data"; +import { LoginData } from "../spec-data/onepassword-1pux/login-data"; +import { MedicalRecordData } from "../spec-data/onepassword-1pux/medical-record"; +import { MembershipData } from "../spec-data/onepassword-1pux/membership"; +import { OnePuxExampleFile } from "../spec-data/onepassword-1pux/onepux_example"; +import { OutdoorLicenseData } from "../spec-data/onepassword-1pux/outdoor-license"; +import { PassportData } from "../spec-data/onepassword-1pux/passport"; +import { PasswordData } from "../spec-data/onepassword-1pux/password"; +import { RewardsProgramData } from "../spec-data/onepassword-1pux/rewards-program"; +import { SanitizedExport } from "../spec-data/onepassword-1pux/sanitized-export"; +import { SecureNoteData } from "../spec-data/onepassword-1pux/secure-note"; +import { ServerData } from "../spec-data/onepassword-1pux/server"; +import { SoftwareLicenseData } from "../spec-data/onepassword-1pux/software-license"; +import { SSNData } from "../spec-data/onepassword-1pux/ssn"; +import { WirelessRouterData } from "../spec-data/onepassword-1pux/wireless-router"; + +import { OnePassword1PuxImporter } from "./onepassword-1pux-importer"; function validateCustomField(fields: FieldView[], fieldName: string, expectedValue: any) { expect(fields).toBeDefined(); diff --git a/libs/importer/spec/onepassword-mac-csv-importer.spec.ts b/libs/importer/src/importers/onepassword/onepassword-mac-csv-importer.spec.ts similarity index 85% rename from libs/importer/spec/onepassword-mac-csv-importer.spec.ts rename to libs/importer/src/importers/onepassword/onepassword-mac-csv-importer.spec.ts index f96d254f4b9f..8a4eeae158a4 100644 --- a/libs/importer/spec/onepassword-mac-csv-importer.spec.ts +++ b/libs/importer/src/importers/onepassword/onepassword-mac-csv-importer.spec.ts @@ -1,11 +1,11 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { OnePasswordMacCsvImporter } from "../src/importers"; +import { data as creditCardData } from "../spec-data/onepassword-csv/credit-card.mac.csv"; +import { data as identityData } from "../spec-data/onepassword-csv/identity.mac.csv"; +import { data as multiTypeData } from "../spec-data/onepassword-csv/multiple-items.mac.csv"; -import { data as creditCardData } from "./test-data/onepassword-csv/credit-card.mac.csv"; -import { data as identityData } from "./test-data/onepassword-csv/identity.mac.csv"; -import { data as multiTypeData } from "./test-data/onepassword-csv/multiple-items.mac.csv"; +import { OnePasswordMacCsvImporter } from "./onepassword-mac-csv-importer"; function expectIdentity(cipher: CipherView) { expect(cipher.type).toBe(CipherType.Identity); diff --git a/libs/importer/spec/onepassword-win-csv-importer.spec.ts b/libs/importer/src/importers/onepassword/onepassword-win-csv-importer.spec.ts similarity index 86% rename from libs/importer/spec/onepassword-win-csv-importer.spec.ts rename to libs/importer/src/importers/onepassword/onepassword-win-csv-importer.spec.ts index 0e7fac351e76..0e63fe698311 100644 --- a/libs/importer/spec/onepassword-win-csv-importer.spec.ts +++ b/libs/importer/src/importers/onepassword/onepassword-win-csv-importer.spec.ts @@ -2,11 +2,11 @@ import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; -import { OnePasswordWinCsvImporter } from "../src/importers"; +import { data as creditCardData } from "../spec-data/onepassword-csv/credit-card.windows.csv"; +import { data as identityData } from "../spec-data/onepassword-csv/identity.windows.csv"; +import { data as multiTypeData } from "../spec-data/onepassword-csv/multiple-items.windows.csv"; -import { data as creditCardData } from "./test-data/onepassword-csv/credit-card.windows.csv"; -import { data as identityData } from "./test-data/onepassword-csv/identity.windows.csv"; -import { data as multiTypeData } from "./test-data/onepassword-csv/multiple-items.windows.csv"; +import { OnePasswordWinCsvImporter } from "./onepassword-win-csv-importer"; function expectIdentity(cipher: CipherView) { expect(cipher.type).toBe(CipherType.Identity); diff --git a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts index 353fcf1337ee..63a0427b2c7d 100644 --- a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts +++ b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts @@ -120,7 +120,7 @@ export interface Value { export interface Email { email_address: string; - provider: string; + provider: string | null; } export interface Address { diff --git a/libs/importer/spec/passky-json-importer.spec.ts b/libs/importer/src/importers/passky/passky-json-importer.spec.ts similarity index 81% rename from libs/importer/spec/passky-json-importer.spec.ts rename to libs/importer/src/importers/passky/passky-json-importer.spec.ts index ddec76876d88..33be2dfac87c 100644 --- a/libs/importer/spec/passky-json-importer.spec.ts +++ b/libs/importer/src/importers/passky/passky-json-importer.spec.ts @@ -1,7 +1,7 @@ -import { PasskyJsonImporter } from "../src/importers"; +import { testData as EncryptedData } from "../spec-data/passky-json/passky-encrypted.json"; +import { testData as UnencryptedData } from "../spec-data/passky-json/passky-unencrypted.json"; -import { testData as EncryptedData } from "./test-data/passky-json/passky-encrypted.json"; -import { testData as UnencryptedData } from "./test-data/passky-json/passky-unencrypted.json"; +import { PasskyJsonImporter } from "./passky-json-importer"; describe("Passky Json Importer", () => { let importer: PasskyJsonImporter; diff --git a/libs/importer/spec/passwordxp-csv-importer.spec.ts b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts similarity index 92% rename from libs/importer/spec/passwordxp-csv-importer.spec.ts rename to libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts index fda323450c6c..1bcc79723abe 100644 --- a/libs/importer/spec/passwordxp-csv-importer.spec.ts +++ b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts @@ -1,13 +1,13 @@ import { CipherType } from "@bitwarden/common/vault/enums"; -import { PasswordXPCsvImporter } from "../src/importers"; -import { ImportResult } from "../src/models/import-result"; - -import { dutchHeaders } from "./test-data/passwordxp-csv/dutch-headers"; -import { germanHeaders } from "./test-data/passwordxp-csv/german-headers"; -import { noFolder } from "./test-data/passwordxp-csv/no-folder.csv"; -import { withFolders } from "./test-data/passwordxp-csv/passwordxp-with-folders.csv"; -import { withoutFolders } from "./test-data/passwordxp-csv/passwordxp-without-folders.csv"; +import { ImportResult } from "../../models/import-result"; +import { dutchHeaders } from "../spec-data/passwordxp-csv/dutch-headers"; +import { germanHeaders } from "../spec-data/passwordxp-csv/german-headers"; +import { noFolder } from "../spec-data/passwordxp-csv/no-folder.csv"; +import { withFolders } from "../spec-data/passwordxp-csv/passwordxp-with-folders.csv"; +import { withoutFolders } from "../spec-data/passwordxp-csv/passwordxp-without-folders.csv"; + +import { PasswordXPCsvImporter } from "./passwordxp-csv-importer"; async function importLoginWithCustomFields(importer: PasswordXPCsvImporter, csvData: string) { const result: ImportResult = await importer.parse(csvData); diff --git a/libs/importer/spec/protonpass-json-importer.spec.ts b/libs/importer/src/importers/protonpass/protonpass-json-importer.spec.ts similarity index 98% rename from libs/importer/spec/protonpass-json-importer.spec.ts rename to libs/importer/src/importers/protonpass/protonpass-json-importer.spec.ts index 39a09127c277..b8550bcb191c 100644 --- a/libs/importer/spec/protonpass-json-importer.spec.ts +++ b/libs/importer/src/importers/protonpass/protonpass-json-importer.spec.ts @@ -4,9 +4,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; -import { ProtonPassJsonImporter } from "../src/importers"; +import { testData } from "../spec-data/protonpass-json/protonpass.json"; -import { testData } from "./test-data/protonpass-json/protonpass.json"; +import { ProtonPassJsonImporter } from "./protonpass-json-importer"; describe("Protonpass Json Importer", () => { let importer: ProtonPassJsonImporter; diff --git a/libs/importer/spec/psono-json-importer.spec.ts b/libs/importer/src/importers/psono/psono-json-importer.spec.ts similarity index 96% rename from libs/importer/spec/psono-json-importer.spec.ts rename to libs/importer/src/importers/psono/psono-json-importer.spec.ts index 82795e5ea5d3..3b4fcf67a30c 100644 --- a/libs/importer/spec/psono-json-importer.spec.ts +++ b/libs/importer/src/importers/psono/psono-json-importer.spec.ts @@ -1,19 +1,19 @@ import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; -import { PsonoJsonImporter } from "../src/importers"; - -import { ApplicationPasswordsData } from "./test-data/psono-json/application-passwords"; -import { BookmarkData } from "./test-data/psono-json/bookmark.json"; -import { EmptyTestFolderData } from "./test-data/psono-json/empty-folders"; -import { EnvVariablesData } from "./test-data/psono-json/environment-variables"; -import { FoldersTestData } from "./test-data/psono-json/folders"; -import { GPGData } from "./test-data/psono-json/gpg"; -import { NotesData } from "./test-data/psono-json/notes"; -import { ReducedWebsiteLoginsData } from "./test-data/psono-json/reduced-website-logins"; -import { SubFoldersTestData } from "./test-data/psono-json/subfolders"; -import { TOTPData } from "./test-data/psono-json/totp"; -import { WebsiteLoginsData } from "./test-data/psono-json/website-logins"; +import { ApplicationPasswordsData } from "../spec-data/psono-json/application-passwords"; +import { BookmarkData } from "../spec-data/psono-json/bookmark.json"; +import { EmptyTestFolderData } from "../spec-data/psono-json/empty-folders"; +import { EnvVariablesData } from "../spec-data/psono-json/environment-variables"; +import { FoldersTestData } from "../spec-data/psono-json/folders"; +import { GPGData } from "../spec-data/psono-json/gpg"; +import { NotesData } from "../spec-data/psono-json/notes"; +import { ReducedWebsiteLoginsData } from "../spec-data/psono-json/reduced-website-logins"; +import { SubFoldersTestData } from "../spec-data/psono-json/subfolders"; +import { TOTPData } from "../spec-data/psono-json/totp"; +import { WebsiteLoginsData } from "../spec-data/psono-json/website-logins"; + +import { PsonoJsonImporter } from "./psono-json-importer"; function validateCustomField( fields: FieldView[], diff --git a/libs/importer/spec/roboform-csv-importer.spec.ts b/libs/importer/src/importers/roboform-csv-importer.spec.ts similarity index 82% rename from libs/importer/spec/roboform-csv-importer.spec.ts rename to libs/importer/src/importers/roboform-csv-importer.spec.ts index 9ec63aadfbc7..2779a9ddb642 100644 --- a/libs/importer/spec/roboform-csv-importer.spec.ts +++ b/libs/importer/src/importers/roboform-csv-importer.spec.ts @@ -1,8 +1,8 @@ -import { CipherType } from "../../common/src/vault/enums/cipher-type"; -import { RoboFormCsvImporter } from "../src/importers"; +import { CipherType } from "@bitwarden/common/vault/enums"; -import { data as dataNoFolder } from "./test-data/roboform-csv/empty-folders"; -import { data as dataFolder } from "./test-data/roboform-csv/with-folders"; +import { RoboFormCsvImporter } from "./roboform-csv-importer"; +import { data as dataNoFolder } from "./spec-data/roboform-csv/empty-folders"; +import { data as dataFolder } from "./spec-data/roboform-csv/with-folders"; describe("Roboform CSV Importer", () => { it("should parse CSV data", async () => { diff --git a/libs/importer/spec/safari-csv-importer.spec.ts b/libs/importer/src/importers/safari-csv-importer.spec.ts similarity index 92% rename from libs/importer/spec/safari-csv-importer.spec.ts rename to libs/importer/src/importers/safari-csv-importer.spec.ts index 4a22eb0bdc4f..4ca8df23f348 100644 --- a/libs/importer/spec/safari-csv-importer.spec.ts +++ b/libs/importer/src/importers/safari-csv-importer.spec.ts @@ -2,10 +2,9 @@ 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 { SafariCsvImporter } from "../src/importers"; - -import { data as oldSimplePasswordData } from "./test-data/safari-csv/old-simple-password-data.csv"; -import { data as simplePasswordData } from "./test-data/safari-csv/simple-password-data.csv"; +import { SafariCsvImporter } from "./safari-csv-importer"; +import { data as oldSimplePasswordData } from "./spec-data/safari-csv/old-simple-password-data.csv"; +import { data as simplePasswordData } from "./spec-data/safari-csv/simple-password-data.csv"; const CipherData = [ { diff --git a/libs/importer/spec/securesafe-csv-importer.spec.ts b/libs/importer/src/importers/securesafe-csv-importer.spec.ts similarity index 93% rename from libs/importer/spec/securesafe-csv-importer.spec.ts rename to libs/importer/src/importers/securesafe-csv-importer.spec.ts index 8d12ae12d942..799c3c6e9d57 100644 --- a/libs/importer/spec/securesafe-csv-importer.spec.ts +++ b/libs/importer/src/importers/securesafe-csv-importer.spec.ts @@ -2,9 +2,8 @@ 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 { SecureSafeCsvImporter } from "../src/importers"; - -import { data_upperUrl, data_lowerUrl } from "./test-data/securesafe-csv/securesafe-example.csv"; +import { SecureSafeCsvImporter } from "./securesafe-csv-importer"; +import { data_upperUrl, data_lowerUrl } from "./spec-data/securesafe-csv/securesafe-example.csv"; const CipherData = [ { diff --git a/libs/importer/spec/test-data/bitwarden-json/account-encrypted.json.ts b/libs/importer/src/importers/spec-data/bitwarden-json/account-encrypted.json.ts similarity index 100% rename from libs/importer/spec/test-data/bitwarden-json/account-encrypted.json.ts rename to libs/importer/src/importers/spec-data/bitwarden-json/account-encrypted.json.ts diff --git a/libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts b/libs/importer/src/importers/spec-data/bitwarden-json/cipher-with-collections.json.ts similarity index 100% rename from libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts rename to libs/importer/src/importers/spec-data/bitwarden-json/cipher-with-collections.json.ts diff --git a/libs/importer/spec/test-data/bitwarden-json/password-protected.json.ts b/libs/importer/src/importers/spec-data/bitwarden-json/password-protected.json.ts similarity index 100% rename from libs/importer/spec/test-data/bitwarden-json/password-protected.json.ts rename to libs/importer/src/importers/spec-data/bitwarden-json/password-protected.json.ts diff --git a/libs/importer/spec/test-data/bitwarden-json/unencrypted.json.ts b/libs/importer/src/importers/spec-data/bitwarden-json/unencrypted.json.ts similarity index 100% rename from libs/importer/spec/test-data/bitwarden-json/unencrypted.json.ts rename to libs/importer/src/importers/spec-data/bitwarden-json/unencrypted.json.ts diff --git a/libs/importer/spec/test-data/chrome-csv/android-data.csv.ts b/libs/importer/src/importers/spec-data/chrome-csv/android-data.csv.ts similarity index 100% rename from libs/importer/spec/test-data/chrome-csv/android-data.csv.ts rename to libs/importer/src/importers/spec-data/chrome-csv/android-data.csv.ts diff --git a/libs/importer/spec/test-data/chrome-csv/simple-password-data.csv.ts b/libs/importer/src/importers/spec-data/chrome-csv/simple-password-data.csv.ts similarity index 100% rename from libs/importer/spec/test-data/chrome-csv/simple-password-data.csv.ts rename to libs/importer/src/importers/spec-data/chrome-csv/simple-password-data.csv.ts diff --git a/libs/importer/spec/test-data/dashlane-csv/credentials-otpurl.csv.ts b/libs/importer/src/importers/spec-data/dashlane-csv/credentials-otpurl.csv.ts similarity index 100% rename from libs/importer/spec/test-data/dashlane-csv/credentials-otpurl.csv.ts rename to libs/importer/src/importers/spec-data/dashlane-csv/credentials-otpurl.csv.ts diff --git a/libs/importer/spec/test-data/dashlane-csv/credentials.csv.ts b/libs/importer/src/importers/spec-data/dashlane-csv/credentials.csv.ts similarity index 100% rename from libs/importer/spec/test-data/dashlane-csv/credentials.csv.ts rename to libs/importer/src/importers/spec-data/dashlane-csv/credentials.csv.ts diff --git a/libs/importer/spec/test-data/dashlane-csv/id.csv.ts b/libs/importer/src/importers/spec-data/dashlane-csv/id.csv.ts similarity index 100% rename from libs/importer/spec/test-data/dashlane-csv/id.csv.ts rename to libs/importer/src/importers/spec-data/dashlane-csv/id.csv.ts diff --git a/libs/importer/spec/test-data/dashlane-csv/multiple-personal-info.csv.ts b/libs/importer/src/importers/spec-data/dashlane-csv/multiple-personal-info.csv.ts similarity index 100% rename from libs/importer/spec/test-data/dashlane-csv/multiple-personal-info.csv.ts rename to libs/importer/src/importers/spec-data/dashlane-csv/multiple-personal-info.csv.ts diff --git a/libs/importer/spec/test-data/dashlane-csv/payments.csv.ts b/libs/importer/src/importers/spec-data/dashlane-csv/payments.csv.ts similarity index 100% rename from libs/importer/spec/test-data/dashlane-csv/payments.csv.ts rename to libs/importer/src/importers/spec-data/dashlane-csv/payments.csv.ts diff --git a/libs/importer/spec/test-data/dashlane-csv/personal-info.csv.ts b/libs/importer/src/importers/spec-data/dashlane-csv/personal-info.csv.ts similarity index 100% rename from libs/importer/spec/test-data/dashlane-csv/personal-info.csv.ts rename to libs/importer/src/importers/spec-data/dashlane-csv/personal-info.csv.ts diff --git a/libs/importer/spec/test-data/dashlane-csv/securenotes.csv.ts b/libs/importer/src/importers/spec-data/dashlane-csv/securenotes.csv.ts similarity index 100% rename from libs/importer/spec/test-data/dashlane-csv/securenotes.csv.ts rename to libs/importer/src/importers/spec-data/dashlane-csv/securenotes.csv.ts diff --git a/libs/importer/spec/test-data/enpass-json/credit-card.ts b/libs/importer/src/importers/spec-data/enpass-json/credit-card.ts similarity index 98% rename from libs/importer/spec/test-data/enpass-json/credit-card.ts rename to libs/importer/src/importers/spec-data/enpass-json/credit-card.ts index 621190a735f6..207e934903f5 100644 --- a/libs/importer/spec/test-data/enpass-json/credit-card.ts +++ b/libs/importer/src/importers/spec-data/enpass-json/credit-card.ts @@ -1,4 +1,4 @@ -import { EnpassJsonFile } from "../../../src/importers/enpass/types/enpass-json-type"; +import { EnpassJsonFile } from "../../enpass/types/enpass-json-type"; export const creditCard: EnpassJsonFile = { folders: [], diff --git a/libs/importer/spec/test-data/enpass-json/folders.ts b/libs/importer/src/importers/spec-data/enpass-json/folders.ts similarity index 91% rename from libs/importer/spec/test-data/enpass-json/folders.ts rename to libs/importer/src/importers/spec-data/enpass-json/folders.ts index 22ab26524b06..9af44f057bfc 100644 --- a/libs/importer/spec/test-data/enpass-json/folders.ts +++ b/libs/importer/src/importers/spec-data/enpass-json/folders.ts @@ -1,4 +1,4 @@ -import { EnpassJsonFile } from "../../../src/importers/enpass/types/enpass-json-type"; +import { EnpassJsonFile } from "../../enpass/types/enpass-json-type"; export const folders: EnpassJsonFile = { folders: [ diff --git a/libs/importer/spec/test-data/enpass-json/login-android-url.ts b/libs/importer/src/importers/spec-data/enpass-json/login-android-url.ts similarity index 95% rename from libs/importer/spec/test-data/enpass-json/login-android-url.ts rename to libs/importer/src/importers/spec-data/enpass-json/login-android-url.ts index 6b0ac41ccde4..91dca5fe99ab 100644 --- a/libs/importer/spec/test-data/enpass-json/login-android-url.ts +++ b/libs/importer/src/importers/spec-data/enpass-json/login-android-url.ts @@ -1,4 +1,5 @@ -import { EnpassJsonFile } from "../../../src/importers/enpass/types/enpass-json-type"; +// @ts-strict-ignore +import { EnpassJsonFile } from "../../enpass/types/enpass-json-type"; import { login } from "./login"; diff --git a/libs/importer/spec/test-data/enpass-json/login.ts b/libs/importer/src/importers/spec-data/enpass-json/login.ts similarity index 97% rename from libs/importer/spec/test-data/enpass-json/login.ts rename to libs/importer/src/importers/spec-data/enpass-json/login.ts index 07707f2ca3e7..6626b8c9e4b6 100644 --- a/libs/importer/spec/test-data/enpass-json/login.ts +++ b/libs/importer/src/importers/spec-data/enpass-json/login.ts @@ -1,4 +1,4 @@ -import { EnpassJsonFile } from "../../../src/importers/enpass/types/enpass-json-type"; +import { EnpassJsonFile } from "../../enpass/types/enpass-json-type"; export const login: EnpassJsonFile = { folders: [], diff --git a/libs/importer/spec/test-data/enpass-json/note.ts b/libs/importer/src/importers/spec-data/enpass-json/note.ts similarity index 86% rename from libs/importer/spec/test-data/enpass-json/note.ts rename to libs/importer/src/importers/spec-data/enpass-json/note.ts index 58f54371ad0d..37c1658b5175 100644 --- a/libs/importer/spec/test-data/enpass-json/note.ts +++ b/libs/importer/src/importers/spec-data/enpass-json/note.ts @@ -1,4 +1,4 @@ -import { EnpassJsonFile } from "../../../src/importers/enpass/types/enpass-json-type"; +import { EnpassJsonFile } from "../../enpass/types/enpass-json-type"; export const note: EnpassJsonFile = { folders: [], diff --git a/libs/importer/spec/test-data/firefox-csv/firefox-accounts-data.csv.ts b/libs/importer/src/importers/spec-data/firefox-csv/firefox-accounts-data.csv.ts similarity index 100% rename from libs/importer/spec/test-data/firefox-csv/firefox-accounts-data.csv.ts rename to libs/importer/src/importers/spec-data/firefox-csv/firefox-accounts-data.csv.ts diff --git a/libs/importer/spec/test-data/firefox-csv/simple-password-data.csv.ts b/libs/importer/src/importers/spec-data/firefox-csv/simple-password-data.csv.ts similarity index 100% rename from libs/importer/spec/test-data/firefox-csv/simple-password-data.csv.ts rename to libs/importer/src/importers/spec-data/firefox-csv/simple-password-data.csv.ts diff --git a/libs/importer/spec/test-data/keepass2-xml/keepass2-xml-importer-testdata.ts b/libs/importer/src/importers/spec-data/keepass2-xml/keepass2-xml-importer-testdata.ts similarity index 100% rename from libs/importer/spec/test-data/keepass2-xml/keepass2-xml-importer-testdata.ts rename to libs/importer/src/importers/spec-data/keepass2-xml/keepass2-xml-importer-testdata.ts diff --git a/libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts b/libs/importer/src/importers/spec-data/keepassx-csv/testdata.csv.ts similarity index 100% rename from libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts rename to libs/importer/src/importers/spec-data/keepassx-csv/testdata.csv.ts diff --git a/libs/importer/spec/test-data/keeper-csv/testdata.csv.ts b/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts similarity index 100% rename from libs/importer/spec/test-data/keeper-csv/testdata.csv.ts rename to libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts diff --git a/libs/importer/spec/test-data/keeper-json/testdata.json.ts b/libs/importer/src/importers/spec-data/keeper-json/testdata.json.ts similarity index 95% rename from libs/importer/spec/test-data/keeper-json/testdata.json.ts rename to libs/importer/src/importers/spec-data/keeper-json/testdata.json.ts index 952ced5027f1..e18789adcb5b 100644 --- a/libs/importer/spec/test-data/keeper-json/testdata.json.ts +++ b/libs/importer/src/importers/spec-data/keeper-json/testdata.json.ts @@ -1,4 +1,4 @@ -import { KeeperJsonExport } from "../../../src/importers/keeper/types/keeper-json-types"; +import { KeeperJsonExport } from "../../keeper/types/keeper-json-types"; export const testData: KeeperJsonExport = { shared_folders: [ diff --git a/libs/importer/spec/test-data/myki-csv/user-account.csv.ts b/libs/importer/src/importers/spec-data/myki-csv/user-account.csv.ts similarity index 100% rename from libs/importer/spec/test-data/myki-csv/user-account.csv.ts rename to libs/importer/src/importers/spec-data/myki-csv/user-account.csv.ts diff --git a/libs/importer/spec/test-data/myki-csv/user-credit-card.csv.ts b/libs/importer/src/importers/spec-data/myki-csv/user-credit-card.csv.ts similarity index 100% rename from libs/importer/spec/test-data/myki-csv/user-credit-card.csv.ts rename to libs/importer/src/importers/spec-data/myki-csv/user-credit-card.csv.ts diff --git a/libs/importer/spec/test-data/myki-csv/user-id-card.csv.ts b/libs/importer/src/importers/spec-data/myki-csv/user-id-card.csv.ts similarity index 100% rename from libs/importer/spec/test-data/myki-csv/user-id-card.csv.ts rename to libs/importer/src/importers/spec-data/myki-csv/user-id-card.csv.ts diff --git a/libs/importer/spec/test-data/myki-csv/user-identity.csv.ts b/libs/importer/src/importers/spec-data/myki-csv/user-identity.csv.ts similarity index 100% rename from libs/importer/spec/test-data/myki-csv/user-identity.csv.ts rename to libs/importer/src/importers/spec-data/myki-csv/user-identity.csv.ts diff --git a/libs/importer/spec/test-data/myki-csv/user-note.csv.ts b/libs/importer/src/importers/spec-data/myki-csv/user-note.csv.ts similarity index 100% rename from libs/importer/spec/test-data/myki-csv/user-note.csv.ts rename to libs/importer/src/importers/spec-data/myki-csv/user-note.csv.ts diff --git a/libs/importer/spec/test-data/myki-csv/user-twofa.csv.ts b/libs/importer/src/importers/spec-data/myki-csv/user-twofa.csv.ts similarity index 100% rename from libs/importer/spec/test-data/myki-csv/user-twofa.csv.ts rename to libs/importer/src/importers/spec-data/myki-csv/user-twofa.csv.ts diff --git a/libs/importer/spec/test-data/netwrix-csv/login-export.csv.ts b/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts similarity index 100% rename from libs/importer/spec/test-data/netwrix-csv/login-export.csv.ts rename to libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts diff --git a/libs/importer/spec/test-data/nordpass-csv/nordpass.card.csv.ts b/libs/importer/src/importers/spec-data/nordpass-csv/nordpass.card.csv.ts similarity index 100% rename from libs/importer/spec/test-data/nordpass-csv/nordpass.card.csv.ts rename to libs/importer/src/importers/spec-data/nordpass-csv/nordpass.card.csv.ts diff --git a/libs/importer/spec/test-data/nordpass-csv/nordpass.identity.csv.ts b/libs/importer/src/importers/spec-data/nordpass-csv/nordpass.identity.csv.ts similarity index 100% rename from libs/importer/spec/test-data/nordpass-csv/nordpass.identity.csv.ts rename to libs/importer/src/importers/spec-data/nordpass-csv/nordpass.identity.csv.ts diff --git a/libs/importer/spec/test-data/nordpass-csv/nordpass.login-with-additinal-urls.csv.ts b/libs/importer/src/importers/spec-data/nordpass-csv/nordpass.login-with-additinal-urls.csv.ts similarity index 100% rename from libs/importer/spec/test-data/nordpass-csv/nordpass.login-with-additinal-urls.csv.ts rename to libs/importer/src/importers/spec-data/nordpass-csv/nordpass.login-with-additinal-urls.csv.ts diff --git a/libs/importer/spec/test-data/nordpass-csv/nordpass.login.csv.ts b/libs/importer/src/importers/spec-data/nordpass-csv/nordpass.login.csv.ts similarity index 100% rename from libs/importer/spec/test-data/nordpass-csv/nordpass.login.csv.ts rename to libs/importer/src/importers/spec-data/nordpass-csv/nordpass.login.csv.ts diff --git a/libs/importer/spec/test-data/nordpass-csv/nordpass.secure-note.csv.ts b/libs/importer/src/importers/spec-data/nordpass-csv/nordpass.secure-note.csv.ts similarity index 100% rename from libs/importer/spec/test-data/nordpass-csv/nordpass.secure-note.csv.ts rename to libs/importer/src/importers/spec-data/nordpass-csv/nordpass.secure-note.csv.ts diff --git a/libs/importer/spec/test-data/onepassword-1pux/api-credentials.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/api-credentials.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/api-credentials.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/api-credentials.ts index 7a830194b8c1..f8047ad8d843 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/api-credentials.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/api-credentials.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const APICredentialsData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/bank-account.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/bank-account.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/bank-account.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/bank-account.ts index 6ffccafe5148..f98e205624bd 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/bank-account.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/bank-account.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const BankAccountData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/credit-card.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/credit-card.ts similarity index 99% rename from libs/importer/spec/test-data/onepassword-1pux/credit-card.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/credit-card.ts index 74af97a4c2bb..5ed4cd8ef1b9 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/credit-card.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/credit-card.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const CreditCardData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/database.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/database.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/database.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/database.ts index 58631a2f98bf..316107e54fce 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/database.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/database.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const DatabaseData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/drivers-license.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/drivers-license.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/drivers-license.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/drivers-license.ts index 54a52f36999e..2c50cd2c42eb 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/drivers-license.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/drivers-license.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const DriversLicenseData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/email-account.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/email-account.ts similarity index 99% rename from libs/importer/spec/test-data/onepassword-1pux/email-account.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/email-account.ts index 0d94973ec295..a164f4bc504c 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/email-account.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/email-account.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const EmailAccountData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/email-field-on-identity.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/email-field-on-identity.ts similarity index 96% rename from libs/importer/spec/test-data/onepassword-1pux/email-field-on-identity.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/email-field-on-identity.ts index 009743dfd08a..8e72088e1701 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/email-field-on-identity.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/email-field-on-identity.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const EmailFieldOnIdentityData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/email-field-on-identity_prefilled.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/email-field-on-identity_prefilled.ts similarity index 96% rename from libs/importer/spec/test-data/onepassword-1pux/email-field-on-identity_prefilled.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/email-field-on-identity_prefilled.ts index 591d27bd2274..79a306faf2c9 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/email-field-on-identity_prefilled.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/email-field-on-identity_prefilled.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const EmailFieldOnIdentityPrefilledData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/email-field.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/email-field.ts similarity index 96% rename from libs/importer/spec/test-data/onepassword-1pux/email-field.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/email-field.ts index f6258bda7fa4..5360c9484ee3 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/email-field.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/email-field.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const EmailFieldData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/identity-data.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/identity-data.ts similarity index 99% rename from libs/importer/spec/test-data/onepassword-1pux/identity-data.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/identity-data.ts index c97a2bbaf458..eb0a55552015 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/identity-data.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/identity-data.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const IdentityData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/login-data.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/login-data.ts similarity index 97% rename from libs/importer/spec/test-data/onepassword-1pux/login-data.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/login-data.ts index dc6a026d8350..6d0515cb8cb2 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/login-data.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/login-data.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const LoginData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/medical-record.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/medical-record.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/medical-record.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/medical-record.ts index 67a929b13b14..5746ca99956e 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/medical-record.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/medical-record.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const MedicalRecordData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/membership.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/membership.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/membership.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/membership.ts index 5e36e8e6c9f1..1801f0a4f928 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/membership.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/membership.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const MembershipData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/onepux_example.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/onepux_example.ts similarity index 96% rename from libs/importer/spec/test-data/onepassword-1pux/onepux_example.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/onepux_example.ts index 38f81783671c..dc7c59c589b5 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/onepux_example.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/onepux_example.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const OnePuxExampleFile: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/outdoor-license.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/outdoor-license.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/outdoor-license.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/outdoor-license.ts index aa8df12948e6..b31bc0300a18 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/outdoor-license.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/outdoor-license.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const OutdoorLicenseData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/passport.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/passport.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/passport.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/passport.ts index 00336aa70cd6..1a22601a3c26 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/passport.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/passport.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const PassportData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/password.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/password.ts similarity index 93% rename from libs/importer/spec/test-data/onepassword-1pux/password.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/password.ts index e416ab9c715d..d9169ab3cabe 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/password.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/password.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const PasswordData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/rewards-program.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/rewards-program.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/rewards-program.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/rewards-program.ts index 75333086435f..223bae6f39d0 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/rewards-program.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/rewards-program.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const RewardsProgramData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/sanitized-export.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/sanitized-export.ts similarity index 99% rename from libs/importer/spec/test-data/onepassword-1pux/sanitized-export.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/sanitized-export.ts index 284921eb80e4..bf1e87c9c1d8 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/sanitized-export.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/sanitized-export.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const SanitizedExport: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/secure-note.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/secure-note.ts similarity index 93% rename from libs/importer/spec/test-data/onepassword-1pux/secure-note.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/secure-note.ts index d7f93c9895c3..0e02b2533be3 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/secure-note.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/secure-note.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const SecureNoteData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/server.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/server.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/server.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/server.ts index 59e7a841f9c0..c74deceb6f92 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/server.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/server.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const ServerData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/software-license.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/software-license.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/software-license.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/software-license.ts index a6ccea530591..0fdf683a4260 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/software-license.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/software-license.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const SoftwareLicenseData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/ssn.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/ssn.ts similarity index 96% rename from libs/importer/spec/test-data/onepassword-1pux/ssn.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/ssn.ts index 4b1c5e137393..1514094e0952 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/ssn.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/ssn.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const SSNData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-1pux/wireless-router.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/wireless-router.ts similarity index 98% rename from libs/importer/spec/test-data/onepassword-1pux/wireless-router.ts rename to libs/importer/src/importers/spec-data/onepassword-1pux/wireless-router.ts index cef06abe6127..b31934843630 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/wireless-router.ts +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/wireless-router.ts @@ -1,4 +1,4 @@ -import { ExportData } from "../../../src/importers/onepassword/types/onepassword-1pux-importer-types"; +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; export const WirelessRouterData: ExportData = { accounts: [ diff --git a/libs/importer/spec/test-data/onepassword-csv/credit-card.mac.csv.ts b/libs/importer/src/importers/spec-data/onepassword-csv/credit-card.mac.csv.ts similarity index 100% rename from libs/importer/spec/test-data/onepassword-csv/credit-card.mac.csv.ts rename to libs/importer/src/importers/spec-data/onepassword-csv/credit-card.mac.csv.ts diff --git a/libs/importer/spec/test-data/onepassword-csv/credit-card.windows.csv.ts b/libs/importer/src/importers/spec-data/onepassword-csv/credit-card.windows.csv.ts similarity index 100% rename from libs/importer/spec/test-data/onepassword-csv/credit-card.windows.csv.ts rename to libs/importer/src/importers/spec-data/onepassword-csv/credit-card.windows.csv.ts diff --git a/libs/importer/spec/test-data/onepassword-csv/identity.mac.csv.ts b/libs/importer/src/importers/spec-data/onepassword-csv/identity.mac.csv.ts similarity index 100% rename from libs/importer/spec/test-data/onepassword-csv/identity.mac.csv.ts rename to libs/importer/src/importers/spec-data/onepassword-csv/identity.mac.csv.ts diff --git a/libs/importer/spec/test-data/onepassword-csv/identity.windows.csv.ts b/libs/importer/src/importers/spec-data/onepassword-csv/identity.windows.csv.ts similarity index 100% rename from libs/importer/spec/test-data/onepassword-csv/identity.windows.csv.ts rename to libs/importer/src/importers/spec-data/onepassword-csv/identity.windows.csv.ts diff --git a/libs/importer/spec/test-data/onepassword-csv/multiple-items.mac.csv.ts b/libs/importer/src/importers/spec-data/onepassword-csv/multiple-items.mac.csv.ts similarity index 100% rename from libs/importer/spec/test-data/onepassword-csv/multiple-items.mac.csv.ts rename to libs/importer/src/importers/spec-data/onepassword-csv/multiple-items.mac.csv.ts diff --git a/libs/importer/spec/test-data/onepassword-csv/multiple-items.windows.csv.ts b/libs/importer/src/importers/spec-data/onepassword-csv/multiple-items.windows.csv.ts similarity index 100% rename from libs/importer/spec/test-data/onepassword-csv/multiple-items.windows.csv.ts rename to libs/importer/src/importers/spec-data/onepassword-csv/multiple-items.windows.csv.ts diff --git a/libs/importer/spec/test-data/passky-json/passky-encrypted.json.ts b/libs/importer/src/importers/spec-data/passky-json/passky-encrypted.json.ts similarity index 85% rename from libs/importer/spec/test-data/passky-json/passky-encrypted.json.ts rename to libs/importer/src/importers/spec-data/passky-json/passky-encrypted.json.ts index 6b9743be343d..a73894ce0c2d 100644 --- a/libs/importer/spec/test-data/passky-json/passky-encrypted.json.ts +++ b/libs/importer/src/importers/spec-data/passky-json/passky-encrypted.json.ts @@ -1,4 +1,4 @@ -import { PasskyJsonExport } from "../../../src/importers/passky/passky-json-types"; +import { PasskyJsonExport } from "../../passky/passky-json-types"; export const testData: PasskyJsonExport = { encrypted: true, diff --git a/libs/importer/spec/test-data/passky-json/passky-unencrypted.json.ts b/libs/importer/src/importers/spec-data/passky-json/passky-unencrypted.json.ts similarity index 73% rename from libs/importer/spec/test-data/passky-json/passky-unencrypted.json.ts rename to libs/importer/src/importers/spec-data/passky-json/passky-unencrypted.json.ts index c382136648a2..738595a6d254 100644 --- a/libs/importer/spec/test-data/passky-json/passky-unencrypted.json.ts +++ b/libs/importer/src/importers/spec-data/passky-json/passky-unencrypted.json.ts @@ -1,4 +1,4 @@ -import { PasskyJsonExport } from "../../../src/importers/passky/passky-json-types"; +import { PasskyJsonExport } from "../../passky/passky-json-types"; export const testData: PasskyJsonExport = { encrypted: false, diff --git a/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts b/libs/importer/src/importers/spec-data/passwordxp-csv/dutch-headers.ts similarity index 100% rename from libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts rename to libs/importer/src/importers/spec-data/passwordxp-csv/dutch-headers.ts diff --git a/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts b/libs/importer/src/importers/spec-data/passwordxp-csv/german-headers.ts similarity index 100% rename from libs/importer/spec/test-data/passwordxp-csv/german-headers.ts rename to libs/importer/src/importers/spec-data/passwordxp-csv/german-headers.ts diff --git a/libs/importer/spec/test-data/passwordxp-csv/no-folder.csv.ts b/libs/importer/src/importers/spec-data/passwordxp-csv/no-folder.csv.ts similarity index 100% rename from libs/importer/spec/test-data/passwordxp-csv/no-folder.csv.ts rename to libs/importer/src/importers/spec-data/passwordxp-csv/no-folder.csv.ts diff --git a/libs/importer/spec/test-data/passwordxp-csv/passwordxp-with-folders.csv.ts b/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts similarity index 100% rename from libs/importer/spec/test-data/passwordxp-csv/passwordxp-with-folders.csv.ts rename to libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts diff --git a/libs/importer/spec/test-data/passwordxp-csv/passwordxp-without-folders.csv.ts b/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-without-folders.csv.ts similarity index 100% rename from libs/importer/spec/test-data/passwordxp-csv/passwordxp-without-folders.csv.ts rename to libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-without-folders.csv.ts diff --git a/libs/importer/spec/test-data/protonpass-json/protonpass.json.ts b/libs/importer/src/importers/spec-data/protonpass-json/protonpass.json.ts similarity index 99% rename from libs/importer/spec/test-data/protonpass-json/protonpass.json.ts rename to libs/importer/src/importers/spec-data/protonpass-json/protonpass.json.ts index 367c2b37e14a..d01cfa0d8f8b 100644 --- a/libs/importer/spec/test-data/protonpass-json/protonpass.json.ts +++ b/libs/importer/src/importers/spec-data/protonpass-json/protonpass.json.ts @@ -1,4 +1,4 @@ -import { ProtonPassJsonFile } from "../../../src/importers/protonpass/types/protonpass-json-type"; +import { ProtonPassJsonFile } from "../../protonpass/types/protonpass-json-type"; export const testData: ProtonPassJsonFile = { version: "1.21.2", diff --git a/libs/importer/spec/test-data/psono-json/application-passwords.ts b/libs/importer/src/importers/spec-data/psono-json/application-passwords.ts similarity index 87% rename from libs/importer/spec/test-data/psono-json/application-passwords.ts rename to libs/importer/src/importers/spec-data/psono-json/application-passwords.ts index 29c4a44e0c98..c224c593d977 100644 --- a/libs/importer/spec/test-data/psono-json/application-passwords.ts +++ b/libs/importer/src/importers/spec-data/psono-json/application-passwords.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const ApplicationPasswordsData: PsonoJsonExport = { folders: [], diff --git a/libs/importer/spec/test-data/psono-json/bookmark.json.ts b/libs/importer/src/importers/spec-data/psono-json/bookmark.json.ts similarity index 86% rename from libs/importer/spec/test-data/psono-json/bookmark.json.ts rename to libs/importer/src/importers/spec-data/psono-json/bookmark.json.ts index fe0726fbe405..b50928cfe7b3 100644 --- a/libs/importer/spec/test-data/psono-json/bookmark.json.ts +++ b/libs/importer/src/importers/spec-data/psono-json/bookmark.json.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const BookmarkData: PsonoJsonExport = { folders: [], diff --git a/libs/importer/spec/test-data/psono-json/empty-folders.ts b/libs/importer/src/importers/spec-data/psono-json/empty-folders.ts similarity index 62% rename from libs/importer/spec/test-data/psono-json/empty-folders.ts rename to libs/importer/src/importers/spec-data/psono-json/empty-folders.ts index 914cba585fe5..28e1d6970947 100644 --- a/libs/importer/spec/test-data/psono-json/empty-folders.ts +++ b/libs/importer/src/importers/spec-data/psono-json/empty-folders.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const EmptyTestFolderData: PsonoJsonExport = { folders: [ diff --git a/libs/importer/spec/test-data/psono-json/environment-variables.ts b/libs/importer/src/importers/spec-data/psono-json/environment-variables.ts similarity index 88% rename from libs/importer/spec/test-data/psono-json/environment-variables.ts rename to libs/importer/src/importers/spec-data/psono-json/environment-variables.ts index 9a26776ebc8a..7ab411d2216b 100644 --- a/libs/importer/spec/test-data/psono-json/environment-variables.ts +++ b/libs/importer/src/importers/spec-data/psono-json/environment-variables.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const EnvVariablesData: PsonoJsonExport = { folders: [], diff --git a/libs/importer/spec/test-data/psono-json/folders.ts b/libs/importer/src/importers/spec-data/psono-json/folders.ts similarity index 96% rename from libs/importer/spec/test-data/psono-json/folders.ts rename to libs/importer/src/importers/spec-data/psono-json/folders.ts index d6e64b7cddb8..5e2c94815153 100644 --- a/libs/importer/spec/test-data/psono-json/folders.ts +++ b/libs/importer/src/importers/spec-data/psono-json/folders.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const FoldersTestData: PsonoJsonExport = { folders: [ diff --git a/libs/importer/spec/test-data/psono-json/gpg.ts b/libs/importer/src/importers/spec-data/psono-json/gpg.ts similarity index 99% rename from libs/importer/spec/test-data/psono-json/gpg.ts rename to libs/importer/src/importers/spec-data/psono-json/gpg.ts index 4015b53aa4e7..57494f4d761c 100644 --- a/libs/importer/spec/test-data/psono-json/gpg.ts +++ b/libs/importer/src/importers/spec-data/psono-json/gpg.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const GPGData: PsonoJsonExport = { folders: [], diff --git a/libs/importer/spec/test-data/psono-json/notes.ts b/libs/importer/src/importers/spec-data/psono-json/notes.ts similarity index 82% rename from libs/importer/spec/test-data/psono-json/notes.ts rename to libs/importer/src/importers/spec-data/psono-json/notes.ts index 0317371456db..0e9859ee1ed6 100644 --- a/libs/importer/spec/test-data/psono-json/notes.ts +++ b/libs/importer/src/importers/spec-data/psono-json/notes.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const NotesData: PsonoJsonExport = { folders: [], diff --git a/libs/importer/spec/test-data/psono-json/reduced-website-logins.ts b/libs/importer/src/importers/spec-data/psono-json/reduced-website-logins.ts similarity index 87% rename from libs/importer/spec/test-data/psono-json/reduced-website-logins.ts rename to libs/importer/src/importers/spec-data/psono-json/reduced-website-logins.ts index 4477c9d52402..9c286f59b525 100644 --- a/libs/importer/spec/test-data/psono-json/reduced-website-logins.ts +++ b/libs/importer/src/importers/spec-data/psono-json/reduced-website-logins.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const ReducedWebsiteLoginsData: PsonoJsonExport = { folders: [], diff --git a/libs/importer/spec/test-data/psono-json/subfolders.ts b/libs/importer/src/importers/spec-data/psono-json/subfolders.ts similarity index 97% rename from libs/importer/spec/test-data/psono-json/subfolders.ts rename to libs/importer/src/importers/spec-data/psono-json/subfolders.ts index eff55581da71..2aff686aa177 100644 --- a/libs/importer/spec/test-data/psono-json/subfolders.ts +++ b/libs/importer/src/importers/spec-data/psono-json/subfolders.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const SubFoldersTestData: PsonoJsonExport = { folders: [ diff --git a/libs/importer/spec/test-data/psono-json/totp.ts b/libs/importer/src/importers/spec-data/psono-json/totp.ts similarity index 85% rename from libs/importer/spec/test-data/psono-json/totp.ts rename to libs/importer/src/importers/spec-data/psono-json/totp.ts index 62d3c43431cf..ec7cabf7ec99 100644 --- a/libs/importer/spec/test-data/psono-json/totp.ts +++ b/libs/importer/src/importers/spec-data/psono-json/totp.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const TOTPData: PsonoJsonExport = { folders: [], diff --git a/libs/importer/spec/test-data/psono-json/website-logins.ts b/libs/importer/src/importers/spec-data/psono-json/website-logins.ts similarity index 90% rename from libs/importer/spec/test-data/psono-json/website-logins.ts rename to libs/importer/src/importers/spec-data/psono-json/website-logins.ts index 85154090492b..28a2f35328e4 100644 --- a/libs/importer/spec/test-data/psono-json/website-logins.ts +++ b/libs/importer/src/importers/spec-data/psono-json/website-logins.ts @@ -1,4 +1,4 @@ -import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; +import { PsonoJsonExport } from "../../psono/psono-json-types"; export const WebsiteLoginsData: PsonoJsonExport = { folders: [], diff --git a/libs/importer/spec/test-data/roboform-csv/empty-folders.ts b/libs/importer/src/importers/spec-data/roboform-csv/empty-folders.ts similarity index 100% rename from libs/importer/spec/test-data/roboform-csv/empty-folders.ts rename to libs/importer/src/importers/spec-data/roboform-csv/empty-folders.ts diff --git a/libs/importer/spec/test-data/roboform-csv/with-folders.ts b/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts similarity index 100% rename from libs/importer/spec/test-data/roboform-csv/with-folders.ts rename to libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts diff --git a/libs/importer/spec/test-data/safari-csv/old-simple-password-data.csv.ts b/libs/importer/src/importers/spec-data/safari-csv/old-simple-password-data.csv.ts similarity index 100% rename from libs/importer/spec/test-data/safari-csv/old-simple-password-data.csv.ts rename to libs/importer/src/importers/spec-data/safari-csv/old-simple-password-data.csv.ts diff --git a/libs/importer/spec/test-data/safari-csv/simple-password-data.csv.ts b/libs/importer/src/importers/spec-data/safari-csv/simple-password-data.csv.ts similarity index 100% rename from libs/importer/spec/test-data/safari-csv/simple-password-data.csv.ts rename to libs/importer/src/importers/spec-data/safari-csv/simple-password-data.csv.ts diff --git a/libs/importer/spec/test-data/safeincloud/test-data.xml.ts b/libs/importer/src/importers/spec-data/safeincloud/test-data.xml.ts similarity index 100% rename from libs/importer/spec/test-data/safeincloud/test-data.xml.ts rename to libs/importer/src/importers/spec-data/safeincloud/test-data.xml.ts diff --git a/libs/importer/spec/test-data/securesafe-csv/securesafe-example.csv.ts b/libs/importer/src/importers/spec-data/securesafe-csv/securesafe-example.csv.ts similarity index 100% rename from libs/importer/spec/test-data/securesafe-csv/securesafe-example.csv.ts rename to libs/importer/src/importers/spec-data/securesafe-csv/securesafe-example.csv.ts diff --git a/libs/importer/spec/test-data/zohovault/sample-zohovault-data.csv.ts b/libs/importer/src/importers/spec-data/zohovault/sample-zohovault-data.csv.ts similarity index 100% rename from libs/importer/spec/test-data/zohovault/sample-zohovault-data.csv.ts rename to libs/importer/src/importers/spec-data/zohovault/sample-zohovault-data.csv.ts diff --git a/libs/importer/spec/zohovault-csv-importer.spec.ts b/libs/importer/src/importers/zohovault-csv-importer.spec.ts similarity index 95% rename from libs/importer/spec/zohovault-csv-importer.spec.ts rename to libs/importer/src/importers/zohovault-csv-importer.spec.ts index 283189452918..e49b527cbbfb 100644 --- a/libs/importer/spec/zohovault-csv-importer.spec.ts +++ b/libs/importer/src/importers/zohovault-csv-importer.spec.ts @@ -2,9 +2,8 @@ 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 { ZohoVaultCsvImporter } from "../src/importers"; - -import { data as samplezohovaultcsvdata } from "./test-data/zohovault/sample-zohovault-data.csv"; +import { data as samplezohovaultcsvdata } from "./spec-data/zohovault/sample-zohovault-data.csv"; +import { ZohoVaultCsvImporter } from "./zohovault-csv-importer"; const CipherData = [ { diff --git a/libs/importer/src/index.ts b/libs/importer/src/index.ts index 4c6c4131ba4a..1d7a88d31625 100644 --- a/libs/importer/src/index.ts +++ b/libs/importer/src/index.ts @@ -1,5 +1,4 @@ export * from "./models"; - export * from "./services"; export { Importer } from "./importers/importer"; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index e8f63ef8705c..4de0bf70b073 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -3,7 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 493713b1d7a9..781b4f75e56e 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -9,11 +9,11 @@ import { } from "@bitwarden/admin-console/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request"; import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request"; import { KvpRequest } from "@bitwarden/common/models/request/kvp.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; diff --git a/libs/importer/tsconfig.json b/libs/importer/tsconfig.json index 2235cccb5c72..e16a16a03377 100644 --- a/libs/importer/tsconfig.json +++ b/libs/importer/tsconfig.json @@ -13,9 +13,10 @@ "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], "@bitwarden/key-management": ["../key-management/src"], "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"] } }, - "include": ["src", "spec"], + "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/key-management-ui/README.md b/libs/key-management-ui/README.md new file mode 100644 index 000000000000..d6b2d699262b --- /dev/null +++ b/libs/key-management-ui/README.md @@ -0,0 +1,3 @@ +# Key management UI + +This lib represents the public API of the Key management team at Bitwarden. Modules are imported using `@bitwarden/key-management-ui`. diff --git a/libs/key-management-ui/jest.config.js b/libs/key-management-ui/jest.config.js new file mode 100644 index 000000000000..9373a8d2aed8 --- /dev/null +++ b/libs/key-management-ui/jest.config.js @@ -0,0 +1,20 @@ +const { pathsToModuleNameMapper } = require("ts-jest"); + +const { compilerOptions } = require("../shared/tsconfig.spec"); + +const sharedConfig = require("../../libs/shared/jest.config.angular"); + +/** @type {import('jest').Config} */ +module.exports = { + ...sharedConfig, + displayName: "libs/key management ui tests", + preset: "jest-preset-angular", + setupFilesAfterEnv: ["/test.setup.ts"], + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in tests + { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "/", + }, + ), +}; diff --git a/libs/key-management-ui/package.json b/libs/key-management-ui/package.json new file mode 100644 index 000000000000..9a05bf07c63b --- /dev/null +++ b/libs/key-management-ui/package.json @@ -0,0 +1,21 @@ +{ + "name": "@bitwarden/key-management-ui", + "version": "0.0.0", + "description": "Common code used across Bitwarden JavaScript projects.", + "keywords": [ + "bitwarden" + ], + "author": "Bitwarden Inc.", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/clients" + }, + "license": "GPL-3.0", + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && tsc", + "build:watch": "npm run clean && tsc -watch", + "test": "jest" + } +} diff --git a/libs/key-management/src/angular/index.ts b/libs/key-management-ui/src/index.ts similarity index 100% rename from libs/key-management/src/angular/index.ts rename to libs/key-management-ui/src/index.ts diff --git a/libs/key-management/src/angular/lock/components/lock.component.html b/libs/key-management-ui/src/lock/components/lock.component.html similarity index 97% rename from libs/key-management/src/angular/lock/components/lock.component.html rename to libs/key-management-ui/src/lock/components/lock.component.html index 7d9ed6124f69..437e29447e2d 100644 --- a/libs/key-management/src/angular/lock/components/lock.component.html +++ b/libs/key-management-ui/src/lock/components/lock.component.html @@ -6,15 +6,13 @@ - + - - diff --git a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts index 7bcffd923992..58da1157f7c2 100644 --- a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -35,7 +34,6 @@ export class CredentialGeneratorHistoryDialogComponent { private accountService: AccountService, private history: GeneratorHistoryService, private dialogService: DialogService, - private dialogRef: DialogRef, ) { this.accountService.activeAccount$ .pipe( @@ -54,11 +52,6 @@ export class CredentialGeneratorHistoryDialogComponent { .subscribe(this.hasHistory$); } - /** closes the dialog */ - protected close() { - this.dialogRef.close(); - } - /** Launches clear history flow */ protected async clear() { const confirmed = await this.dialogService.openSimpleDialog({ diff --git a/libs/tools/generator/components/src/credential-generator-history.component.ts b/libs/tools/generator/components/src/credential-generator-history.component.ts index 69ed0b0336dc..7e476564de61 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history.component.ts @@ -72,6 +72,6 @@ export class CredentialGeneratorHistoryComponent { protected getGeneratedValueText(credential: GeneratedCredential) { const info = this.generatorService.algorithm(credential.category); - return info.generatedValue; + return info.credentialType; } } diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index 6f27c7020dba..624c5ab5860e 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -20,7 +20,7 @@ type="button" bitIconButton="bwi-generate" buttonType="main" - (click)="generate('user request')" + (click)="generate(USER_REQUEST)" [appA11yTitle]="credentialTypeGenerateLabel$ | async" [disabled]="!(algorithm$ | async)" > diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts index a2b204eaca4c..04c7e5e8d87c 100644 --- a/libs/tools/generator/components/src/credential-generator.component.ts +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LiveAnnouncer } from "@angular/cdk/a11y"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { @@ -28,6 +29,7 @@ import { CredentialAlgorithm, CredentialCategory, CredentialGeneratorService, + GenerateRequest, GeneratedCredential, Generators, getForwarderConfiguration, @@ -60,6 +62,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { private accountService: AccountService, private zone: NgZone, private formBuilder: FormBuilder, + private ariaLive: LiveAnnouncer, ) {} /** Binds the component to a specific user's settings. When this input is not provided, @@ -185,10 +188,10 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // continue with origin stream return generator; }), - withLatestFrom(this.userId$), + withLatestFrom(this.userId$, this.algorithm$), takeUntil(this.destroyed), ) - .subscribe(([generated, userId]) => { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -198,8 +201,12 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + + this.generatedCredential$.next(generated); this.onGenerated.next(generated); - this.value$.next(generated.credential); }); }); @@ -383,7 +390,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { this.zone.run(() => { if (!a || a.onlyOnRequest) { - this.value$.next("-"); + this.generatedCredential$.next(null); } else { this.generate("autogenerate").catch((e: unknown) => this.logService.error(e)); } @@ -391,6 +398,10 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { }); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + private typeToGenerator$(type: CredentialAlgorithm) { const dependencies = { on$: this.generate$, @@ -473,7 +484,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); /** Emits hint key for the currently selected credential type */ @@ -482,21 +493,28 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { /** tracks the currently selected credential category */ protected category$ = new ReplaySubject(1); + private readonly generatedCredential$ = new BehaviorSubject(null); + /** Emits the last generated value. */ - protected readonly value$ = new BehaviorSubject(""); + protected readonly value$ = this.generatedCredential$.pipe( + map((generated) => generated?.credential ?? "-"), + ); /** Emits when the userId changes */ protected readonly userId$ = new BehaviorSubject(null); + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; + /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } private toOptions(algorithms: AlgorithmInfo[]) { @@ -515,7 +533,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // finalize subjects this.generate$.complete(); - this.value$.complete(); + this.generatedCredential$.complete(); // finalize component bindings this.onGenerated.complete(); diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts index a4e751095d4c..f60f4c5b0953 100644 --- a/libs/tools/generator/components/src/generator-services.module.ts +++ b/libs/tools/generator/components/src/generator-services.module.ts @@ -6,7 +6,7 @@ import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider"; diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index a6aa5ebdd02d..c7fa93dc535c 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -18,7 +18,7 @@ type="button" bitIconButton="bwi-generate" buttonType="main" - (click)="generate('user request')" + (click)="generate(USER_REQUEST)" [appA11yTitle]="credentialTypeGenerateLabel$ | async" [disabled]="!(algorithm$ | async)" > diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index 85363412ffa3..b59b162e6876 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LiveAnnouncer } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { @@ -16,7 +17,6 @@ import { } from "rxjs"; 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 { UserId } from "@bitwarden/common/types/guid"; import { ToastService, Option } from "@bitwarden/components"; @@ -28,6 +28,7 @@ import { isPasswordAlgorithm, AlgorithmInfo, isSameAlgorithm, + GenerateRequest, } from "@bitwarden/generator-core"; import { GeneratorHistoryService } from "@bitwarden/generator-history"; @@ -42,9 +43,9 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { private generatorHistoryService: GeneratorHistoryService, private toastService: ToastService, private logService: LogService, - private i18nService: I18nService, private accountService: AccountService, private zone: NgZone, + private ariaLive: LiveAnnouncer, ) {} /** Binds the component to a specific user's settings. @@ -67,14 +68,17 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { protected readonly userId$ = new BehaviorSubject(null); /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); + + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } /** Tracks changes to the selected credential type @@ -137,10 +141,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // continue with origin stream return generator; }), - withLatestFrom(this.userId$), + withLatestFrom(this.userId$, this.algorithm$), takeUntil(this.destroyed), ) - .subscribe(([generated, userId]) => { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -150,6 +154,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + this.onGenerated.next(generated); this.value$.next(generated.credential); }); @@ -205,6 +213,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { }); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + private typeToGenerator$(type: CredentialAlgorithm) { const dependencies = { on$: this.generate$, @@ -249,7 +261,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); private toOptions(algorithms: AlgorithmInfo[]) { diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index a5effcc0f992..20cb0ec31bd1 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -7,7 +7,7 @@ type="button" bitIconButton="bwi-generate" buttonType="main" - (click)="generate('user request')" + (click)="generate(USER_REQUEST)" [appA11yTitle]="credentialTypeGenerateLabel$ | async" [disabled]="!(algorithm$ | async)" > diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index 63c1adc602ba..e521fea8e281 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LiveAnnouncer } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; @@ -28,6 +29,7 @@ import { AlgorithmInfo, CredentialAlgorithm, CredentialGeneratorService, + GenerateRequest, GeneratedCredential, Generators, getForwarderConfiguration, @@ -66,6 +68,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { private accountService: AccountService, private zone: NgZone, private formBuilder: FormBuilder, + private ariaLive: LiveAnnouncer, ) {} /** Binds the component to a specific user's settings. When this input is not provided, @@ -160,10 +163,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // continue with origin stream return generator; }), - withLatestFrom(this.userId$), + withLatestFrom(this.userId$, this.algorithm$), takeUntil(this.destroyed), ) - .subscribe(([generated, userId]) => { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -173,6 +176,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + this.onGenerated.next(generated); this.value$.next(generated.credential); }); @@ -360,6 +367,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { throw new Error(`Invalid generator type: "${type}"`); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + /** Lists the credential types supported by the component. */ protected typeOptions$ = new BehaviorSubject[]>([]); @@ -375,6 +386,18 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { /** tracks the currently selected credential type */ protected algorithm$ = new ReplaySubject(1); + /** Emits hint key for the currently selected credential type */ + protected credentialTypeHint$ = new ReplaySubject(1); + + /** Emits the last generated value. */ + protected readonly value$ = new BehaviorSubject(""); + + /** Emits when the userId changes */ + protected readonly userId$ = new BehaviorSubject(null); + + /** Emits when a new credential is requested */ + private readonly generate$ = new Subject(); + protected showAlgorithm$ = this.algorithm$.pipe( combineLatestWith(this.showForwarder$), map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)), @@ -401,27 +424,18 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); - /** Emits hint key for the currently selected credential type */ - protected credentialTypeHint$ = new ReplaySubject(1); - - /** Emits the last generated value. */ - protected readonly value$ = new BehaviorSubject(""); - - /** Emits when the userId changes */ - protected readonly userId$ = new BehaviorSubject(null); - - /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } private toOptions(algorithms: AlgorithmInfo[]) { diff --git a/libs/tools/generator/components/tsconfig.json b/libs/tools/generator/components/tsconfig.json index 76b060334f62..e0e4da268daf 100644 --- a/libs/tools/generator/components/tsconfig.json +++ b/libs/tools/generator/components/tsconfig.json @@ -10,7 +10,8 @@ "@bitwarden/generator-core": ["../../../tools/generator/core/src"], "@bitwarden/generator-history": ["../../../tools/generator/extensions/history/src"], "@bitwarden/key-management": ["../../../key-management/src"], - "@bitwarden/platform": ["../../../platform/src"] + "@bitwarden/platform": ["../../../platform/src"], + "@bitwarden/ui-common": ["../../../ui/common/src"] } }, "include": ["src"], diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts index 92b20b02b755..da87c60f1f44 100644 --- a/libs/tools/generator/core/src/data/generators.ts +++ b/libs/tools/generator/core/src/data/generators.ts @@ -56,9 +56,10 @@ const PASSPHRASE: CredentialGeneratorConfiguration< category: "password", nameKey: "passphrase", generateKey: "generatePassphrase", - generatedValueKey: "passphrase", + onGeneratedMessageKey: "passphraseGenerated", + credentialTypeKey: "passphrase", copyKey: "copyPassphrase", - useGeneratedValueKey: "useThisPassphrase", + useGeneratedValueKey: "useThisPassword", onlyOnRequest: false, request: [], engine: { @@ -118,7 +119,8 @@ const PASSWORD: CredentialGeneratorConfiguration< category: "password", nameKey: "password", generateKey: "generatePassword", - generatedValueKey: "password", + onGeneratedMessageKey: "passwordGenerated", + credentialTypeKey: "password", copyKey: "copyPassword", useGeneratedValueKey: "useThisPassword", onlyOnRequest: false, @@ -195,7 +197,8 @@ const USERNAME: CredentialGeneratorConfiguration; generate( - request: GenerationRequest, + request: GenerateRequest, settings: SubaddressGenerationOptions, ): Promise; async generate( - _request: GenerationRequest, + request: GenerateRequest, settings: CatchallGenerationOptions | SubaddressGenerationOptions, ) { if (isCatchallGenerationOptions(settings)) { const email = await this.randomAsciiCatchall(settings.catchallDomain); - return new GeneratedCredential(email, "catchall", Date.now()); + return new GeneratedCredential( + email, + "catchall", + Date.now(), + request.source, + request.website, + ); } else if (isSubaddressGenerationOptions(settings)) { const email = await this.randomAsciiSubaddress(settings.subaddressEmail); - return new GeneratedCredential(email, "subaddress", Date.now()); + return new GeneratedCredential( + email, + "subaddress", + Date.now(), + request.source, + request.website, + ); } throw new Error("Invalid settings received by generator."); diff --git a/libs/tools/generator/core/src/engine/password-randomizer.ts b/libs/tools/generator/core/src/engine/password-randomizer.ts index c1e9aed7b8bd..a9612d2fb450 100644 --- a/libs/tools/generator/core/src/engine/password-randomizer.ts +++ b/libs/tools/generator/core/src/engine/password-randomizer.ts @@ -1,10 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; -import { GenerationRequest } from "@bitwarden/common/tools/types"; import { CredentialGenerator, + GenerateRequest, GeneratedCredential, PassphraseGenerationOptions, PasswordGenerationOptions, @@ -69,27 +69,39 @@ export class PasswordRandomizer } generate( - request: GenerationRequest, + request: GenerateRequest, settings: PasswordGenerationOptions, ): Promise; generate( - request: GenerationRequest, + request: GenerateRequest, settings: PassphraseGenerationOptions, ): Promise; async generate( - _request: GenerationRequest, + request: GenerateRequest, settings: PasswordGenerationOptions | PassphraseGenerationOptions, ) { if (isPasswordGenerationOptions(settings)) { - const request = optionsToRandomAsciiRequest(settings); - const password = await this.randomAscii(request); - - return new GeneratedCredential(password, "password", Date.now()); + const req = optionsToRandomAsciiRequest(settings); + const password = await this.randomAscii(req); + + return new GeneratedCredential( + password, + "password", + Date.now(), + request.source, + request.website, + ); } else if (isPassphraseGenerationOptions(settings)) { - const request = optionsToEffWordListRequest(settings); - const passphrase = await this.randomEffLongWords(request); - - return new GeneratedCredential(passphrase, "passphrase", Date.now()); + const req = optionsToEffWordListRequest(settings); + const passphrase = await this.randomEffLongWords(req); + + return new GeneratedCredential( + passphrase, + "passphrase", + Date.now(), + request.source, + request.website, + ); } throw new Error("Invalid settings received by generator."); diff --git a/libs/tools/generator/core/src/engine/username-randomizer.ts b/libs/tools/generator/core/src/engine/username-randomizer.ts index df6085538396..d13066c7e556 100644 --- a/libs/tools/generator/core/src/engine/username-randomizer.ts +++ b/libs/tools/generator/core/src/engine/username-randomizer.ts @@ -1,7 +1,11 @@ import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; -import { GenerationRequest } from "@bitwarden/common/tools/types"; -import { CredentialGenerator, EffUsernameGenerationOptions, GeneratedCredential } from "../types"; +import { + CredentialGenerator, + EffUsernameGenerationOptions, + GenerateRequest, + GeneratedCredential, +} from "../types"; import { Randomizer } from "./abstractions"; import { WordsRequest } from "./types"; @@ -51,14 +55,20 @@ export class UsernameRandomizer implements CredentialGenerator { return { generate: (request, settings) => { - const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo; - const result = new GeneratedCredential(credential, SomeAlgorithm, SomeTime); + const result = new GeneratedCredential( + settings.foo, + SomeAlgorithm, + SomeTime, + request.source, + request.website, + ); return Promise.resolve(result); }, }; @@ -191,30 +201,8 @@ describe("CredentialGeneratorService", () => { }); describe("generate$", () => { - it("emits a generation for the active user when subscribed", async () => { - const settings = { foo: "value" }; - await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); - - const result = await generated.expectEmission(); - - expect(result).toEqual(new GeneratedCredential("value", SomeAlgorithm, SomeTime)); - }); - - it("follows the active user", async () => { - const someSettings = { foo: "some value" }; - const anotherSettings = { foo: "another value" }; - await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); - await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); + it("completes when `on$` completes", async () => { + await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -224,22 +212,24 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); + const on$ = new Subject(); + let complete = false; - await accountService.switchAccount(AnotherUser); - await generated.pauseUntilReceived(2); - generated.unsubscribe(); + // confirm no emission during subscription + generator.generate$(SomeConfiguration, { on$ }).subscribe({ + complete: () => { + complete = true; + }, + }); + on$.complete(); + await awaitAsync(); - expect(generated.emissions).toEqual([ - new GeneratedCredential("some value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another value", SomeAlgorithm, SomeTime), - ]); + expect(complete).toBeTruthy(); }); - it("emits a generation when the settings change", async () => { - const someSettings = { foo: "some value" }; - const anotherSettings = { foo: "another value" }; - await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); + it("includes request.source in the generated credential", async () => { + const settings = { foo: "value" }; + await stateProvider.setUserState(SettingsKey, settings, SomeUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -249,23 +239,15 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); + const on$ = new BehaviorSubject({ source: "some source" }); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); - await stateProvider.setUserState(SettingsKey, anotherSettings, SomeUser); - await generated.pauseUntilReceived(2); - generated.unsubscribe(); + const result = await generated.expectEmission(); - expect(generated.emissions).toEqual([ - new GeneratedCredential("some value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another value", SomeAlgorithm, SomeTime), - ]); + expect(result.source).toEqual("some source"); }); - // FIXME: test these when the fake state provider can create the required emissions - it.todo("errors when the settings error"); - it.todo("completes when the settings complete"); - - it("includes `website$`'s last emitted value", async () => { + it("includes request.website in the generated credential", async () => { const settings = { foo: "value" }; await stateProvider.setUserState(SettingsKey, settings, SomeUser); const generator = new CredentialGeneratorService( @@ -277,18 +259,19 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const website$ = new BehaviorSubject("some website"); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ })); + const on$ = new BehaviorSubject({ website: "some website" }); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); const result = await generated.expectEmission(); - expect(result).toEqual( - new GeneratedCredential("some website|value", SomeAlgorithm, SomeTime), - ); + expect(result.website).toEqual("some website"); }); - it("errors when `website$` errors", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); + it("uses the active user's settings", async () => { + const someSettings = { foo: "some value" }; + const anotherSettings = { foo: "another value" }; + await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); + await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -298,44 +281,23 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const website$ = new BehaviorSubject("some website"); - let error = null; + const on$ = new BehaviorSubject({}); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); - generator.generate$(SomeConfiguration, { website$ }).subscribe({ - error: (e: unknown) => { - error = e; - }, - }); - website$.error({ some: "error" }); - await awaitAsync(); + await accountService.switchAccount(AnotherUser); + on$.next({}); + await generated.pauseUntilReceived(2); + generated.unsubscribe(); - expect(error).toEqual({ some: "error" }); + expect(generated.emissions).toEqual([ + new GeneratedCredential("some value", SomeAlgorithm, SomeTime), + new GeneratedCredential("another value", SomeAlgorithm, SomeTime), + ]); }); - it("completes when `website$` completes", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const website$ = new BehaviorSubject("some website"); - let completed = false; - - generator.generate$(SomeConfiguration, { website$ }).subscribe({ - complete: () => { - completed = true; - }, - }); - website$.complete(); - await awaitAsync(); - - expect(completed).toBeTruthy(); - }); + // FIXME: test these when the fake state provider can create the required emissions + it.todo("errors when the settings error"); + it.todo("completes when the settings complete"); it("emits a generation for a specific user when `user$` supplied", async () => { await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); @@ -350,38 +312,17 @@ describe("CredentialGeneratorService", () => { accountService, ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); + const on$ = new Subject(); + const generated = new ObservableTracker( + generator.generate$(SomeConfiguration, { on$, userId$ }), + ); + on$.next({}); const result = await generated.expectEmission(); expect(result).toEqual(new GeneratedCredential("another", SomeAlgorithm, SomeTime)); }); - it("emits a generation for a specific user when `user$` emits", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const userId = new BehaviorSubject(SomeUser); - const userId$ = userId.pipe(filter((u) => !!u)); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); - - userId.next(AnotherUser); - const result = await generated.pauseUntilReceived(2); - - expect(result).toEqual([ - new GeneratedCredential("value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another", SomeAlgorithm, SomeTime), - ]); - }); - it("errors when `user$` errors", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); const generator = new CredentialGeneratorService( @@ -393,10 +334,11 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); + const on$ = new Subject(); const userId$ = new BehaviorSubject(SomeUser); let error = null; - generator.generate$(SomeConfiguration, { userId$ }).subscribe({ + generator.generate$(SomeConfiguration, { on$, userId$ }).subscribe({ error: (e: unknown) => { error = e; }, @@ -418,10 +360,11 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); + const on$ = new Subject(); const userId$ = new BehaviorSubject(SomeUser); let completed = false; - generator.generate$(SomeConfiguration, { userId$ }).subscribe({ + generator.generate$(SomeConfiguration, { on$, userId$ }).subscribe({ complete: () => { completed = true; }, @@ -444,7 +387,7 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const on$ = new Subject(); + const on$ = new Subject(); const results: any[] = []; // confirm no emission during subscription @@ -455,7 +398,7 @@ describe("CredentialGeneratorService", () => { expect(results.length).toEqual(0); // confirm forwarded emission - on$.next(); + on$.next({}); await awaitAsync(); expect(results).toEqual([new GeneratedCredential("value", SomeAlgorithm, SomeTime)]); @@ -465,7 +408,7 @@ describe("CredentialGeneratorService", () => { expect(results.length).toBe(1); // confirm forwarded emission takes latest value - on$.next(); + on$.next({}); await awaitAsync(); sub.unsubscribe(); @@ -486,7 +429,7 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const on$ = new Subject(); + const on$ = new Subject(); let error: any = null; // confirm no emission during subscription @@ -501,35 +444,8 @@ describe("CredentialGeneratorService", () => { expect(error).toEqual({ some: "error" }); }); - it("completes when `on$` completes", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const on$ = new Subject(); - let complete = false; - - // confirm no emission during subscription - generator.generate$(SomeConfiguration, { on$ }).subscribe({ - complete: () => { - complete = true; - }, - }); - on$.complete(); - await awaitAsync(); - - expect(complete).toBeTruthy(); - }); - // FIXME: test these when the fake state provider can delay its first emission it.todo("emits when settings$ become available if on$ is called before they're ready."); - it.todo("emits when website$ become available if on$ is called before they're ready."); }); describe("algorithms", () => { diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index 9659076ec0c3..6e80049870cc 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -2,20 +2,15 @@ // @ts-strict-ignore import { BehaviorSubject, - combineLatest, - concat, concatMap, distinctUntilChanged, endWith, filter, - first, firstValueFrom, ignoreElements, map, Observable, ReplaySubject, - share, - skipUntil, switchMap, takeUntil, withLatestFrom, @@ -34,9 +29,9 @@ import { SingleUserDependency, UserDependency, } from "@bitwarden/common/tools/dependencies"; -import { IntegrationId, IntegrationMetadata } from "@bitwarden/common/tools/integration"; +import { IntegrationMetadata } from "@bitwarden/common/tools/integration"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; -import { anyComplete } from "@bitwarden/common/tools/rx"; +import { anyComplete, withLatestReady } from "@bitwarden/common/tools/rx"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; import { UserId } from "@bitwarden/common/types/guid"; @@ -57,6 +52,7 @@ import { CredentialPreference, isForwarderIntegration, ForwarderIntegration, + GenerateRequest, } from "../types"; import { CredentialGeneratorConfiguration as Configuration, @@ -69,19 +65,7 @@ import { PREFERENCES } from "./credential-preferences"; type Policy$Dependencies = UserDependency; type Settings$Dependencies = Partial; -type Generate$Dependencies = Simplify & Partial> & { - /** Emits the active website when subscribed. - * - * The generator does not respond to emissions of this interface; - * If it is provided, the generator blocks until a value becomes available. - * When `website$` is omitted, the generator uses the empty string instead. - * When `website$` completes, the generator completes. - * When `website$` errors, the generator forwards the error. - */ - website$?: Observable; - - integration$?: Observable; -}; +type Generate$Dependencies = Simplify & Partial>; type Algorithms$Dependencies = Partial; @@ -111,43 +95,20 @@ export class CredentialGeneratorService { /** Generates a stream of credentials * @param configuration determines which generator's settings are loaded - * @param dependencies.on$ when specified, a new credential is emitted when - * this emits. Otherwise, a new credential is emitted when the settings - * update. + * @param dependencies.on$ Required. A new credential is emitted when this emits. */ generate$( configuration: Readonly>, - dependencies?: Generate$Dependencies, + dependencies: Generate$Dependencies, ) { - // instantiate the engine const engine = configuration.engine.create(this.getDependencyProvider()); - - // stream blocks until all of these values are received - const website$ = dependencies?.website$ ?? new BehaviorSubject(null); - const request$ = website$.pipe(map((website) => ({ website }))); const settings$ = this.settings$(configuration, dependencies); - // if on$ triggers before settings are loaded, trigger as soon - // as they become available. - let readyOn$: Observable = null; - if (dependencies?.on$) { - const NO_EMISSIONS = {}; - const ready$ = combineLatest([settings$, request$]).pipe( - first(null, NO_EMISSIONS), - filter((value) => value !== NO_EMISSIONS), - share(), - ); - readyOn$ = concat( - dependencies.on$?.pipe(switchMap(() => ready$)), - dependencies.on$.pipe(skipUntil(ready$)), - ); - } - // generation proper - const generate$ = (readyOn$ ?? settings$).pipe( - withLatestFrom(request$, settings$), - concatMap(([, request, settings]) => engine.generate(request, settings)), - takeUntil(anyComplete([request$, settings$])), + const generate$ = dependencies.on$.pipe( + withLatestReady(settings$), + concatMap(([request, settings]) => engine.generate(request, settings)), + takeUntil(anyComplete([settings$])), ); return generate$; @@ -256,7 +217,8 @@ export class CredentialGeneratorService { category: generator.category, name: integration ? integration.name : this.i18nService.t(generator.nameKey), generate: this.i18nService.t(generator.generateKey), - generatedValue: this.i18nService.t(generator.generatedValueKey), + onGeneratedMessage: this.i18nService.t(generator.onGeneratedMessageKey), + credentialType: this.i18nService.t(generator.credentialTypeKey), copy: this.i18nService.t(generator.copyKey), useGeneratedValue: this.i18nService.t(generator.useGeneratedValueKey), onlyOnRequest: generator.onlyOnRequest, diff --git a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts index f57a1e5f2b65..99834a254175 100644 --- a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts @@ -5,7 +5,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; import { BufferedState } from "@bitwarden/common/tools/state/buffered-state"; diff --git a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.ts b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.ts index c42821738176..1914b27697b3 100644 --- a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.ts +++ b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.ts @@ -4,7 +4,7 @@ import { filter, map } from "rxjs"; import { Jsonify } from "type-fest"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; import { UserKeyEncryptor } from "@bitwarden/common/tools/cryptography/user-key-encryptor"; diff --git a/libs/tools/generator/core/src/types/credential-generator-configuration.ts b/libs/tools/generator/core/src/types/credential-generator-configuration.ts index 0650ae5d34dd..08aec48a9e76 100644 --- a/libs/tools/generator/core/src/types/credential-generator-configuration.ts +++ b/libs/tools/generator/core/src/types/credential-generator-configuration.ts @@ -34,6 +34,9 @@ export type AlgorithmInfo = { /* Localized generate button label */ generate: string; + /** Localized "credential generated" informational message */ + onGeneratedMessage: string; + /* Localized copy button label */ copy: string; @@ -41,7 +44,7 @@ export type AlgorithmInfo = { useGeneratedValue: string; /* Localized generated value label */ - generatedValue: string; + credentialType: string; /** Localized algorithm description */ description?: string; @@ -79,17 +82,22 @@ export type CredentialGeneratorInfo = { /** Localization key for the credential description*/ descriptionKey?: string; - /* Localization key for the generate command label */ + /** Localization key for the generate command label */ generateKey: string; - /* Localization key for the copy button label */ + /** Localization key for the copy button label */ copyKey: string; - /* Localized "use generated credential" button label */ + /** Localization key for the "credential generated" informational message */ + onGeneratedMessageKey: string; + + /** Localized "use generated credential" button label */ useGeneratedValueKey: string; - /* Localization key for describing values generated by this generator */ - generatedValueKey: string; + /** Localization key for describing the kind of credential generated + * by this generator. + */ + credentialTypeKey: string; /** When true, credential generation must be explicitly requested. * @remarks this property is useful when credential generation diff --git a/libs/tools/generator/core/src/types/credential-generator.ts b/libs/tools/generator/core/src/types/credential-generator.ts index c95ff25afffc..c421bbbff874 100644 --- a/libs/tools/generator/core/src/types/credential-generator.ts +++ b/libs/tools/generator/core/src/types/credential-generator.ts @@ -1,5 +1,4 @@ -import { GenerationRequest } from "@bitwarden/common/tools/types"; - +import { GenerateRequest } from "./generate-request"; import { GeneratedCredential } from "./generated-credential"; /** An algorithm that generates credentials. */ @@ -8,5 +7,5 @@ export type CredentialGenerator = { * @param request runtime parameters * @param settings stored parameters */ - generate: (request: GenerationRequest, settings: Settings) => Promise; + generate: (request: GenerateRequest, settings: Settings) => Promise; }; diff --git a/libs/tools/generator/core/src/types/generate-request.ts b/libs/tools/generator/core/src/types/generate-request.ts new file mode 100644 index 000000000000..c7d5bf9c41c1 --- /dev/null +++ b/libs/tools/generator/core/src/types/generate-request.ts @@ -0,0 +1,24 @@ +/** Contextual information about the application state when a generator is invoked. + */ +export type GenerateRequest = { + /** Traces the origin of the generation request. This parameter is + * copied to the generated credential. + * + * @remarks This parameter it is provided solely so that generator + * consumers can differentiate request sources from one another. + * It never affects the random output of the generator algorithms, + * and it is never communicated to 3rd party systems. It MAY be + * tracked in the generator history. + */ + source?: string; + + /** Traces the website associated with a generated credential. + * + * @remarks Random generators MUST NOT depend upon the website during credential + * generation. Non-random generators MAY include the website in the generated + * credential (e.g. a catchall email address). This parameter MAY be transmitted + * to 3rd party systems (e.g. as the description for a forwarding email). + * It MAY be tracked in the generator history. + */ + website?: string; +}; diff --git a/libs/tools/generator/core/src/types/generated-credential.ts b/libs/tools/generator/core/src/types/generated-credential.ts index 6d18a1c7892c..99b864b9fd8e 100644 --- a/libs/tools/generator/core/src/types/generated-credential.ts +++ b/libs/tools/generator/core/src/types/generated-credential.ts @@ -11,11 +11,15 @@ export class GeneratedCredential { * @param generationDate The date that the credential was generated. * Numeric values should are interpreted using {@link Date.valueOf} * semantics. + * @param source traces the origin of the request that generated this credential. + * @param website traces the website associated with the generated credential. */ constructor( readonly credential: string, readonly category: CredentialAlgorithm, generationDate: Date | number, + readonly source?: string, + readonly website?: string, ) { if (typeof generationDate === "number") { this.generationDate = new Date(generationDate); @@ -25,7 +29,7 @@ export class GeneratedCredential { } /** The date that the credential was generated */ - generationDate: Date; + readonly generationDate: Date; /** Constructs a credential from its `toJSON` representation */ static fromJSON(jsonValue: Jsonify) { @@ -38,6 +42,9 @@ export class GeneratedCredential { /** Serializes a credential to a JSON-compatible object */ toJSON() { + // omits the source and website because they were introduced to solve + // UI bugs and it's not yet known whether there's a desire to support + // them in the generator history view. return { credential: this.credential, category: this.category, diff --git a/libs/tools/generator/core/src/types/index.ts b/libs/tools/generator/core/src/types/index.ts index 48272cbf6025..3e392257b0c7 100644 --- a/libs/tools/generator/core/src/types/index.ts +++ b/libs/tools/generator/core/src/types/index.ts @@ -6,6 +6,7 @@ export * from "./credential-generator"; export * from "./credential-generator-configuration"; export * from "./eff-username-generator-options"; export * from "./forwarder-options"; +export * from "./generate-request"; export * from "./generator-constraints"; export * from "./generated-credential"; export * from "./generator-options"; diff --git a/libs/tools/generator/extensions/history/src/legacy-password-history-decryptor.ts b/libs/tools/generator/extensions/history/src/legacy-password-history-decryptor.ts index 6a27ad476ae3..06113ee9b99e 100644 --- a/libs/tools/generator/extensions/history/src/legacy-password-history-decryptor.ts +++ b/libs/tools/generator/extensions/history/src/legacy-password-history-decryptor.ts @@ -1,4 +1,4 @@ -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; diff --git a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts index 3936b03acc94..b3dee69bdbfa 100644 --- a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts +++ b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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"; diff --git a/libs/tools/generator/extensions/history/src/local-generator-history.service.ts b/libs/tools/generator/extensions/history/src/local-generator-history.service.ts index 149a83db42f6..cbfa55a184f1 100644 --- a/libs/tools/generator/extensions/history/src/local-generator-history.service.ts +++ b/libs/tools/generator/extensions/history/src/local-generator-history.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { filter, map } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; import { UserKeyEncryptor } from "@bitwarden/common/tools/cryptography/user-key-encryptor"; import { BufferedState } from "@bitwarden/common/tools/state/buffered-state"; diff --git a/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts b/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts index ae6b74564e3f..0df37617b881 100644 --- a/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts +++ b/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { engine, services, strategies } from "@bitwarden/generator-core"; import { LocalGeneratorHistoryService } from "@bitwarden/generator-history"; diff --git a/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts b/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts index c995fb1bce19..36b4a20aec76 100644 --- a/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts +++ b/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts @@ -3,7 +3,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts index 636b7546af8d..4b569532220f 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts @@ -4,11 +4,13 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; @@ -25,7 +27,7 @@ import { ToastService, TypographyModule, } from "@bitwarden/components"; -import { CredentialGeneratorService, Generators } from "@bitwarden/generator-core"; +import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core"; import { SendFormConfig } from "../../abstractions/send-form-config.service"; import { SendFormContainer } from "../../send-form-container"; @@ -89,11 +91,14 @@ export class SendOptionsComponent implements OnInit { private i18nService: I18nService, private toastService: ToastService, private generatorService: CredentialGeneratorService, + private accountService: AccountService, ) { this.sendFormContainer.registerChildForm("sendOptionsForm", this.sendOptionsForm); - this.policyService - .getAll$(PolicyType.SendOptions) + + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntilDestroyed(), ) @@ -116,8 +121,9 @@ export class SendOptionsComponent implements OnInit { } generatePassword = async () => { + const on$ = new BehaviorSubject({ source: "send" }); const generatedCredential = await firstValueFrom( - this.generatorService.generate$(Generators.password), + this.generatorService.generate$(Generators.password, { on$ }), ); this.sendOptionsForm.patchValue({ diff --git a/libs/tools/send/send-ui/tsconfig.json b/libs/tools/send/send-ui/tsconfig.json index 671154e0a045..e6d6680ad40e 100644 --- a/libs/tools/send/send-ui/tsconfig.json +++ b/libs/tools/send/send-ui/tsconfig.json @@ -13,7 +13,8 @@ "@bitwarden/generator-legacy": ["../../../tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../../../tools/generator/extensions/navigation/src"], "@bitwarden/key-management": ["../../../key-management/src"], - "@bitwarden/platform": ["../../../platform/src"] + "@bitwarden/platform": ["../../../platform/src"], + "@bitwarden/ui-common": ["../../../ui/common/src"] } }, "include": ["src"], diff --git a/libs/ui/README.md b/libs/ui/README.md new file mode 100644 index 000000000000..e245aac71cb8 --- /dev/null +++ b/libs/ui/README.md @@ -0,0 +1,5 @@ +# UI Foundation + +Core UI libraries maintained by the UI Foundation team. + +- _ui-common_: Low-level utilities for Angular applications. diff --git a/libs/ui/common/package.json b/libs/ui/common/package.json new file mode 100644 index 000000000000..f1b03a3ebd07 --- /dev/null +++ b/libs/ui/common/package.json @@ -0,0 +1,15 @@ +{ + "name": "@bitwarden/ui-common", + "version": "0.0.0", + "description": "Low-level utilities for Angular applications", + "keywords": [ + "bitwarden" + ], + "author": "Bitwarden Inc.", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/clients" + }, + "license": "GPL-3.0" +} diff --git a/libs/ui/common/src/di/index.ts b/libs/ui/common/src/di/index.ts new file mode 100644 index 000000000000..7b0705f13645 --- /dev/null +++ b/libs/ui/common/src/di/index.ts @@ -0,0 +1,2 @@ +export * from "./safe-injection-token"; +export * from "./safe-provider"; diff --git a/libs/ui/common/src/di/safe-injection-token.ts b/libs/ui/common/src/di/safe-injection-token.ts new file mode 100644 index 000000000000..aad081e1c765 --- /dev/null +++ b/libs/ui/common/src/di/safe-injection-token.ts @@ -0,0 +1,14 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { InjectionToken } from "@angular/core"; + +declare const tag: unique symbol; +/** + * A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter. + * @remarks The default angular implementation does not use the generic type to define the structure of the object, + * so the structural type system will not complain about a mismatch in the type parameter. + * This is solved by assigning T to an arbitrary private property. + */ +export class SafeInjectionToken extends InjectionToken { + private readonly [tag]: T; +} diff --git a/libs/ui/common/src/di/safe-provider.ts b/libs/ui/common/src/di/safe-provider.ts new file mode 100644 index 000000000000..002aa69a500d --- /dev/null +++ b/libs/ui/common/src/di/safe-provider.ts @@ -0,0 +1,138 @@ +import { Provider } from "@angular/core"; +import { Constructor, Opaque } from "type-fest"; + +import { SafeInjectionToken } from "./safe-injection-token"; + +/** + * The return type of the {@link safeProvider} helper function. + * Used to distinguish a type safe provider definition from a non-type safe provider definition. + */ +export type SafeProvider = Opaque; + +// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0 +type AbstractConstructor = abstract new (...args: any) => T; + +type MapParametersToDeps = { + [K in keyof T]: AbstractConstructor | SafeInjectionToken; +}; + +type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; + +/** + * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken + */ +type ProviderInstanceType = + T extends SafeInjectionToken + ? InstanceType> + : T extends Constructor | AbstractConstructor + ? InstanceType + : never; + +/** + * Represents a dependency provided with the useClass option. + */ +type SafeClassProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends Constructor>, + D extends MapParametersToDeps>, +> = { + provide: A; + useClass: I; + deps: D; +}; + +/** + * Represents a dependency provided with the useValue option. + */ +type SafeValueProvider, V extends SafeInjectionTokenType> = { + provide: A; + useValue: V; +}; + +/** + * Represents a dependency provided with the useFactory option. + */ +type SafeFactoryProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends (...args: any) => ProviderInstanceType, + D extends MapParametersToDeps>, +> = { + provide: A; + useFactory: I; + deps: D; + multi?: boolean; +}; + +/** + * Represents a dependency provided with the useExisting option. + */ +type SafeExistingProvider< + A extends Constructor | AbstractConstructor | SafeInjectionToken, + I extends Constructor> | AbstractConstructor>, +> = { + provide: A; + useExisting: I; +}; + +/** + * Represents a dependency where there is no abstract token, the token is the implementation + */ +type SafeConcreteProvider< + I extends Constructor, + D extends MapParametersToDeps>, +> = { + provide: I; + deps: D; +}; + +/** + * If useAngularDecorators: true is specified, do not require a deps array. + * This is a manual override for where @Injectable decorators are used + */ +type UseAngularDecorators = Omit & { + useAngularDecorators: true; +}; + +/** + * Represents a type with a deps array that may optionally be overridden with useAngularDecorators + */ +type AllowAngularDecorators = T | UseAngularDecorators; + +/** + * A factory function that creates a provider for the ngModule providers array. + * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. + * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, + * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. + * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) + * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) + * @returns The exact same object without modification (pass-through). + */ +export const safeProvider = < + // types for useClass + AClass extends AbstractConstructor | SafeInjectionToken, + IClass extends Constructor>, + DClass extends MapParametersToDeps>, + // types for useValue + AValue extends SafeInjectionToken, + VValue extends SafeInjectionTokenType, + // types for useFactory + AFactory extends AbstractConstructor | SafeInjectionToken, + IFactory extends (...args: any) => ProviderInstanceType, + DFactory extends MapParametersToDeps>, + // types for useExisting + AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, + IExisting extends + | Constructor> + | AbstractConstructor>, + // types for no token + IConcrete extends Constructor, + DConcrete extends MapParametersToDeps>, +>( + provider: + | AllowAngularDecorators> + | SafeValueProvider + | AllowAngularDecorators> + | SafeExistingProvider + | AllowAngularDecorators> + | Constructor, +): SafeProvider => provider as SafeProvider; diff --git a/libs/angular/src/platform/utils/safe-provider.type.spec.ts b/libs/ui/common/src/di/safe-provider.type.spec.ts similarity index 96% rename from libs/angular/src/platform/utils/safe-provider.type.spec.ts rename to libs/ui/common/src/di/safe-provider.type.spec.ts index 6fe6d0d0b6cb..afc7071af1ed 100644 --- a/libs/angular/src/platform/utils/safe-provider.type.spec.ts +++ b/libs/ui/common/src/di/safe-provider.type.spec.ts @@ -11,7 +11,7 @@ class FooFactory { } abstract class FooService { - createFoo: (str: string) => string; + abstract createFoo(str: string): string; } class DefaultFooService implements FooService { @@ -29,7 +29,7 @@ class BarFactory { } abstract class BarService { - createBar: (num: number) => number; + abstract createBar(num: number): number; } class DefaultBarService implements BarService { diff --git a/libs/components/src/shared/i18n.pipe.ts b/libs/ui/common/src/i18n.pipe.ts similarity index 77% rename from libs/components/src/shared/i18n.pipe.ts rename to libs/ui/common/src/i18n.pipe.ts index 91bf0b3198da..fdcfec0ceac4 100644 --- a/libs/components/src/shared/i18n.pipe.ts +++ b/libs/ui/common/src/i18n.pipe.ts @@ -3,7 +3,13 @@ import { Pipe, PipeTransform } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; /** - * Temporarily duplicate this pipe + * Localizes the specified string. + * + * @example + * {{ 'key' | i18n }} + * + * @example + * {{ 'key' | i18n: 'param1' }} */ @Pipe({ name: "i18n", diff --git a/libs/ui/common/src/index.ts b/libs/ui/common/src/index.ts new file mode 100644 index 000000000000..97e551081164 --- /dev/null +++ b/libs/ui/common/src/index.ts @@ -0,0 +1,2 @@ +export * from "./di"; +export * from "./i18n.pipe"; diff --git a/libs/ui/common/tsconfig.json b/libs/ui/common/tsconfig.json new file mode 100644 index 000000000000..31062d41a1c4 --- /dev/null +++ b/libs/ui/common/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/common/*": ["../../common/src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/vault/src/cipher-form/cipher-form-container.ts b/libs/vault/src/cipher-form/cipher-form-container.ts index 54f194598e5e..a5f2289b0c68 100644 --- a/libs/vault/src/cipher-form/cipher-form-container.ts +++ b/libs/vault/src/cipher-form/cipher-form-container.ts @@ -14,7 +14,6 @@ import { SshKeySectionComponent } from "./components/sshkey-section/sshkey-secti /** * The complete form for a cipher. Includes all the sub-forms from their respective section components. - * TODO: Add additional form sections as they are implemented. */ export type CipherForm = { itemDetails?: ItemDetailsSectionComponent["itemDetailsForm"]; @@ -57,4 +56,12 @@ export abstract class CipherFormContainer { * @param updateFn - A function that takes the current cipherView and returns the updated cipherView */ abstract patchCipher(updateFn: (current: CipherView) => CipherView): void; + + /** + * Returns initial values for the CipherView, either from the config or the cached cipher + */ + abstract getInitialCipherView(): CipherView | null; + + /** Returns true when the `CipherFormContainer` was initialized with a cached cipher available. */ + abstract initializedWithCachedCipher(): boolean; } diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index f7146776b73c..1af73b5a8b8f 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -1,4 +1,6 @@ -import { importProvidersFrom } from "@angular/core"; +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { importProvidersFrom, signal } from "@angular/core"; import { action } from "@storybook/addon-actions"; import { applicationConfig, @@ -10,6 +12,7 @@ import { import { BehaviorSubject } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -18,6 +21,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { ClientType } from "@bitwarden/common/enums"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -39,6 +43,7 @@ import { CipherFormService } from "./abstractions/cipher-form.service"; import { TotpCaptureService } from "./abstractions/totp-capture.service"; import { CipherFormModule } from "./cipher-form.module"; import { CipherFormComponent } from "./components/cipher-form.component"; +import { CipherFormCacheService } from "./services/default-cipher-form-cache.service"; const defaultConfig: CipherFormConfig = { mode: "add", @@ -192,6 +197,25 @@ export default { activeAccount$: new BehaviorSubject({ email: "test@example.com" }), }, }, + { + provide: CipherFormCacheService, + useValue: { + getCachedCipherView: (): null => null, + initializedWithValue: false, + }, + }, + { + provide: ViewCacheService, + useValue: { + signal: () => signal(null), + }, + }, + { + provide: ConfigService, + useValue: { + getFeatureFlag: () => Promise.resolve(false), + }, + }, ], }), componentWrapperDecorator( diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts index 15784f1ca06a..705c170933a1 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts @@ -27,9 +27,12 @@ describe("AdditionalOptionsSectionComponent", () => { let passwordRepromptService: MockProxy; let passwordRepromptEnabled$: BehaviorSubject; + const getInitialCipherView = jest.fn(() => null); + beforeEach(async () => { - cipherFormProvider = mock(); + getInitialCipherView.mockClear(); + cipherFormProvider = mock({ getInitialCipherView }); passwordRepromptService = mock(); passwordRepromptEnabled$ = new BehaviorSubject(true); passwordRepromptService.enabled$ = passwordRepromptEnabled$; @@ -94,11 +97,11 @@ describe("AdditionalOptionsSectionComponent", () => { expect(component.additionalOptionsForm.disabled).toBe(true); }); - it("initializes 'additionalOptionsForm' with original cipher view values", () => { - (cipherFormProvider.originalCipherView as any) = { + it("initializes 'additionalOptionsForm' from `getInitialCipherValue`", () => { + getInitialCipherView.mockReturnValueOnce({ notes: "original notes", reprompt: 1, - } as CipherView; + } as CipherView); component.ngOnInit(); diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts index 04291a3e0832..9c619ca2f846 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts @@ -77,11 +77,12 @@ export class AdditionalOptionsSectionComponent implements OnInit { } ngOnInit() { - if (this.cipherFormContainer.originalCipherView) { + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { this.additionalOptionsForm.patchValue({ - notes: this.cipherFormContainer.originalCipherView.notes, - reprompt: - this.cipherFormContainer.originalCipherView.reprompt === CipherRepromptType.Password, + notes: prefillCipher.notes, + reprompt: prefillCipher.reprompt === CipherRepromptType.Password, }); } diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts index f1e5af2868a1..ea756d17c402 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts @@ -25,9 +25,11 @@ describe("AutofillOptionsComponent", () => { let domainSettingsService: MockProxy; let autofillSettingsService: MockProxy; let platformUtilsService: MockProxy; + const getInitialCipherView = jest.fn(() => null); beforeEach(async () => { - cipherFormContainer = mock(); + getInitialCipherView.mockClear(); + cipherFormContainer = mock({ getInitialCipherView }); liveAnnouncer = mock(); platformUtilsService = mock(); domainSettingsService = mock(); @@ -107,12 +109,14 @@ describe("AutofillOptionsComponent", () => { existingLogin.uri = "https://example.com"; existingLogin.match = UriMatchStrategy.Exact; - (cipherFormContainer.originalCipherView as CipherView) = new CipherView(); - cipherFormContainer.originalCipherView.login = { + const cipher = new CipherView(); + cipher.login = { autofillOnPageLoad: true, uris: [existingLogin], } as LoginView; + getInitialCipherView.mockReturnValueOnce(cipher); + fixture.detectChanges(); expect(component.autofillOptionsForm.value.uris).toEqual([ @@ -138,12 +142,14 @@ describe("AutofillOptionsComponent", () => { existingLogin.uri = "https://example.com"; existingLogin.match = UriMatchStrategy.Exact; - (cipherFormContainer.originalCipherView as CipherView) = new CipherView(); - cipherFormContainer.originalCipherView.login = { + const cipher = new CipherView(); + cipher.login = { autofillOnPageLoad: true, uris: [existingLogin], } as LoginView; + getInitialCipherView.mockReturnValueOnce(cipher); + fixture.detectChanges(); expect(component.autofillOptionsForm.value.uris).toEqual([ @@ -159,12 +165,14 @@ describe("AutofillOptionsComponent", () => { existingLogin.uri = "https://example.com"; existingLogin.match = UriMatchStrategy.Exact; - (cipherFormContainer.originalCipherView as CipherView) = new CipherView(); - cipherFormContainer.originalCipherView.login = { + const cipher = new CipherView(); + cipher.login = { autofillOnPageLoad: true, uris: [existingLogin], } as LoginView; + getInitialCipherView.mockReturnValueOnce(cipher); + fixture.detectChanges(); expect(component.autofillOptionsForm.value.uris).toEqual([ diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index cf73f3dbed4a..c3b2a0fb9f9a 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -130,8 +130,9 @@ export class AutofillOptionsComponent implements OnInit { } ngOnInit() { - if (this.cipherFormContainer.originalCipherView?.login) { - this.initFromExistingCipher(this.cipherFormContainer.originalCipherView.login); + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + if (prefillCipher) { + this.initFromExistingCipher(prefillCipher.login); } else { this.initNewCipher(); } @@ -142,17 +143,29 @@ export class AutofillOptionsComponent implements OnInit { } private initFromExistingCipher(existingLogin: LoginView) { + // The `uris` control is a FormArray which needs to dynamically + // add controls to the form. Doing this will trigger the `valueChanges` observable on the form + // and overwrite the `autofillOnPageLoad` value before it is set in the following `patchValue` call. + // Pass `false` to `addUri` to stop events from emitting when adding the URIs. existingLogin.uris?.forEach((uri) => { - this.addUri({ - uri: uri.uri, - matchDetection: uri.match, - }); + this.addUri( + { + uri: uri.uri, + matchDetection: uri.match, + }, + false, + false, + ); }); this.autofillOptionsForm.patchValue({ autofillOnPageLoad: existingLogin.autofillOnPageLoad, }); - if (this.cipherFormContainer.config.initialValues?.loginUri) { + // Only add the initial value when the cipher was not initialized from a cached state + if ( + this.cipherFormContainer.config.initialValues?.loginUri && + !this.cipherFormContainer.initializedWithCachedCipher() + ) { // Avoid adding the same uri again if it already exists if ( existingLogin.uris?.findIndex( @@ -197,9 +210,16 @@ export class AutofillOptionsComponent implements OnInit { * Adds a new URI input to the form. * @param uriFieldValue The initial value for the new URI input. * @param focusNewInput If true, the new URI input will be focused after being added. + * @param emitEvent When false, prevents the `valueChanges` & `statusChanges` observables from firing. */ - addUri(uriFieldValue: UriField = { uri: null, matchDetection: null }, focusNewInput = false) { - this.autofillOptionsForm.controls.uris.push(this.formBuilder.control(uriFieldValue)); + addUri( + uriFieldValue: UriField = { uri: null, matchDetection: null }, + focusNewInput = false, + emitEvent = true, + ) { + this.autofillOptionsForm.controls.uris.push(this.formBuilder.control(uriFieldValue), { + emitEvent, + }); if (focusNewInput) { this.focusOnNewInput$.next(); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts index e9581859b2e4..39a59192985c 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts @@ -20,8 +20,10 @@ describe("CardDetailsSectionComponent", () => { let registerChildFormSpy: jest.SpyInstance; let patchCipherSpy: jest.SpyInstance; + const getInitialCipherView = jest.fn(() => null); + beforeEach(async () => { - cipherFormProvider = mock(); + cipherFormProvider = mock({ getInitialCipherView }); registerChildFormSpy = jest.spyOn(cipherFormProvider, "registerChildForm"); patchCipherSpy = jest.spyOn(cipherFormProvider, "patchCipher"); @@ -94,7 +96,7 @@ describe("CardDetailsSectionComponent", () => { expect(component.cardDetailsForm.disabled).toBe(true); }); - it("initializes `cardDetailsForm` with current values", () => { + it("initializes `cardDetailsForm` from `getInitialCipherValue`", () => { const cardholderName = "Ron Burgundy"; const number = "4242 4242 4242 4242"; const code = "619"; @@ -105,9 +107,7 @@ describe("CardDetailsSectionComponent", () => { cardView.code = code; cardView.brand = "Visa"; - component.originalCipherView = { - card: cardView, - } as CipherView; + getInitialCipherView.mockReturnValueOnce({ card: cardView }); component.ngOnInit(); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts index 63c5906b5709..c2b3ebb59aa7 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts @@ -136,8 +136,10 @@ export class CardDetailsSectionComponent implements OnInit { } ngOnInit() { - if (this.originalCipherView?.card) { - this.setInitialValues(); + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { + this.setInitialValues(prefillCipher); } if (this.disabled) { @@ -172,8 +174,8 @@ export class CardDetailsSectionComponent implements OnInit { } /** Set form initial form values from the current cipher */ - private setInitialValues() { - const { cardholderName, number, brand, expMonth, expYear, code } = this.originalCipherView.card; + private setInitialValues(cipherView: CipherView) { + const { cardholderName, number, brand, expMonth, expYear, code } = cipherView.card; this.cardDetailsForm.setValue({ cardholderName: cardholderName, diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 3d3e04864e89..7fa212dec8a8 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -38,6 +38,7 @@ import { import { CipherFormConfig } from "../abstractions/cipher-form-config.service"; import { CipherFormService } from "../abstractions/cipher-form.service"; import { CipherForm, CipherFormContainer } from "../cipher-form-container"; +import { CipherFormCacheService } from "../services/default-cipher-form-cache.service"; import { AdditionalOptionsSectionComponent } from "./additional-options/additional-options-section.component"; import { CardDetailsSectionComponent } from "./card-details-section/card-details-section.component"; @@ -55,6 +56,9 @@ import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.componen provide: CipherFormContainer, useExisting: forwardRef(() => CipherFormComponent), }, + { + provide: CipherFormCacheService, + }, ], imports: [ AsyncActionsModule, @@ -164,6 +168,26 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci */ patchCipher(updateFn: (current: CipherView) => CipherView): void { this.updatedCipherView = updateFn(this.updatedCipherView); + // Cache the updated cipher + this.cipherFormCacheService.cacheCipherView(this.updatedCipherView); + } + + /** + * Return initial values for given keys of a cipher + */ + getInitialCipherView(): CipherView { + const cachedCipherView = this.cipherFormCacheService.getCachedCipherView(); + + if (cachedCipherView && this.initializedWithCachedCipher()) { + return cachedCipherView; + } + + return this.originalCipherView; + } + + /** */ + initializedWithCachedCipher(): boolean { + return this.cipherFormCacheService.initializedWithValue; } /** @@ -187,6 +211,8 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci // Force change detection so that all child components are destroyed and re-created this.changeDetectorRef.detectChanges(); + await this.cipherFormCacheService.init(); + this.updatedCipherView = new CipherView(); this.originalCipherView = null; this.cipherForm = this.formBuilder.group({}); @@ -220,16 +246,39 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci } } + this.setInitialCipherFromCache(); + this.loading = false; this.formReadySubject.next(); } + /** + * Updates `updatedCipherView` based on the value from the cache. + */ + setInitialCipherFromCache() { + const cachedCipher = this.cipherFormCacheService.getCachedCipherView(); + if (cachedCipher === null) { + return; + } + + // Use the cached cipher when it matches the cipher being edited + if (this.updatedCipherView.id === cachedCipher.id) { + this.updatedCipherView = cachedCipher; + } + + // `id` is null when a cipher is being added + if (this.updatedCipherView.id === null) { + this.updatedCipherView = cachedCipher; + } + } + constructor( private formBuilder: FormBuilder, private addEditFormService: CipherFormService, private toastService: ToastService, private i18nService: I18nService, private changeDetectorRef: ChangeDetectorRef, + private cipherFormCacheService: CipherFormCacheService, ) {} /** diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html index 16bccebb939b..a73cc31652c2 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html @@ -2,11 +2,11 @@ *ngIf="type === 'password'" [disableMargin]="disableMargin" (onGenerated)="onCredentialGenerated($event)" - (onAlgorithm)="algorithm($event)" + (onAlgorithm)="onAlgorithmSelected($event)" > diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index fdde5e15d919..ff705b2d1bab 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -19,7 +19,7 @@ import { AlgorithmInfo, GeneratedCredential } from "@bitwarden/generator-core"; }) export class CipherFormGeneratorComponent { @Input() - algorithm: (selected: AlgorithmInfo) => void; + onAlgorithmSelected: (selected: AlgorithmInfo) => void; /** * The type of generator form to show. diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index 63941c6a4673..fab3c8f1ab1e 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -46,7 +46,7 @@

{{ "customFields" | i18n }}

bitSuffix bitPasswordInputToggle data-testid="visibility-for-custom-hidden-field" - [disabled]="!canViewPasswords(i)" + *ngIf="canViewPasswords(i)" (toggledChange)="logHiddenEvent($event)" > diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts index a5e57d2858f5..d84387d1564b 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts @@ -17,11 +17,8 @@ import { } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; -import { DialogService } from "@bitwarden/components"; +import { BitPasswordInputToggleDirective, DialogService } from "@bitwarden/components"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { BitPasswordInputToggleDirective } from "../../../../../components/src/form-field/password-input-toggle.directive"; import { CipherFormConfig } from "../../abstractions/cipher-form-config.service"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -61,7 +58,13 @@ describe("CustomFieldsComponent", () => { }, { provide: CipherFormContainer, - useValue: { patchCipher, originalCipherView, registerChildForm: jest.fn(), config }, + useValue: { + patchCipher, + originalCipherView, + registerChildForm: jest.fn(), + config, + getInitialCipherView: jest.fn(() => originalCipherView), + }, }, { provide: LiveAnnouncer, @@ -109,11 +112,30 @@ describe("CustomFieldsComponent", () => { value: true, newField: false, }, - { linkedId: 1, name: "linked label", type: FieldType.Linked, value: null, newField: false }, + { + linkedId: 1, + name: "linked label", + type: FieldType.Linked, + value: null, + newField: false, + }, ]); }); - it("forbids a user to view hidden fields when the cipher `viewPassword` is false", () => { + it("when `viewPassword` is false the user cannot see the view toggle option", () => { + originalCipherView.viewPassword = false; + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); + + expect(button).toBeFalsy(); + }); + + it("should disable the hidden field input when `viewPassword` is false", () => { originalCipherView.viewPassword = false; originalCipherView.fields = mockFieldViews; @@ -121,9 +143,22 @@ describe("CustomFieldsComponent", () => { fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('[data-testid="custom-hidden-field"]')); + + expect(input.nativeElement.disabled).toBe(true); + }); + + it("when `viewPassword` is true the user can see the view toggle option", () => { + originalCipherView.viewPassword = true; + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + fixture.detectChanges(); + const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); - expect(button.nativeElement.disabled).toBe(true); + expect(button).toBeTruthy(); }); describe("linkedFieldOptions", () => { diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts index d723bad5751d..bdb96f4327dd 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -127,8 +127,9 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { this.destroyed$ = inject(DestroyRef); this.cipherFormContainer.registerChildForm("customFields", this.customFieldsForm); - this.customFieldsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((values) => { - this.updateCipher(values.fields); + this.customFieldsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => { + // getRawValue ensures disabled fields are included + this.updateCipher(this.fields.getRawValue()); }); } @@ -148,23 +149,32 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { value: id, })); - // Populate the form with the existing fields - this.cipherFormContainer.originalCipherView?.fields?.forEach((field) => { + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + // When available, populate the form with the existing fields + prefillCipher?.fields?.forEach((field) => { let value: string | boolean = field.value; if (field.type === FieldType.Boolean) { value = field.value === "true" ? true : false; } - this.fields.push( - this.formBuilder.group({ - type: field.type, - name: field.name, - value: value, - linkedId: field.linkedId, - newField: false, - }), - ); + const customField = this.formBuilder.group({ + type: field.type, + name: field.name, + value: value, + linkedId: field.linkedId, + newField: false, + }); + + if ( + field.type === FieldType.Hidden && + !this.cipherFormContainer.originalCipherView?.viewPassword + ) { + customField.controls.value.disable(); + } + + this.fields.push(customField); }); // Disable the form if in partial-edit mode diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index b1403231ecf8..2d0608bf38a7 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -113,8 +113,10 @@ export class IdentitySectionComponent implements OnInit { this.identityForm.disable(); } - if (this.originalCipherView && this.originalCipherView.id) { - this.populateFormData(); + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { + this.populateFormData(prefillCipher); } else { this.identityForm.patchValue({ username: this.cipherFormContainer.config.initialValues?.username || "", @@ -122,8 +124,9 @@ export class IdentitySectionComponent implements OnInit { } } - populateFormData() { - const { identity } = this.originalCipherView; + populateFormData(cipherView: CipherView) { + const { identity } = cipherView; + this.identityForm.setValue({ title: identity.title, firstName: identity.firstName, diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html index 648539932de1..9750983bba14 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html @@ -61,8 +61,8 @@

{{ "itemDetails" | i18n }}

formControlName="collectionIds" [baseItems]="collectionOptions" > - - {{ "cannotRemoveViewOnlyCollections" | i18n: readOnlyCollections.join(", ") }} + + {{ "cannotRemoveViewOnlyCollections" | i18n: readOnlyCollectionsNames.join(", ") }}
diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index 32c1e7417e4c..3995422944c7 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -9,7 +9,6 @@ import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SelectComponent } from "@bitwarden/components"; @@ -18,6 +17,29 @@ import { CipherFormContainer } from "../../cipher-form-container"; import { ItemDetailsSectionComponent } from "./item-details-section.component"; +const createMockCollection = ( + id: string, + name: string, + organizationId: string, + readOnly = false, + canEdit = true, +) => { + return { + id, + name, + organizationId, + externalId: "", + readOnly, + hidePasswords: false, + manage: true, + assigned: true, + canEditItems: jest.fn().mockReturnValue(canEdit), + canEdit: jest.fn(), + canDelete: jest.fn(), + canViewCollectionInfo: jest.fn(), + }; +}; + describe("ItemDetailsSectionComponent", () => { let component: ItemDetailsSectionComponent; let fixture: ComponentFixture; @@ -25,9 +47,17 @@ describe("ItemDetailsSectionComponent", () => { let i18nService: MockProxy; const activeAccount$ = new BehaviorSubject<{ email: string }>({ email: "test@example.com" }); + const getInitialCipherView = jest.fn(() => null); + const initializedWithCachedCipher = jest.fn(() => false); beforeEach(async () => { - cipherFormProvider = mock(); + getInitialCipherView.mockClear(); + initializedWithCachedCipher.mockClear(); + + cipherFormProvider = mock({ + getInitialCipherView, + initializedWithCachedCipher, + }); i18nService = mock(); await TestBed.configureTestingModule({ @@ -87,21 +117,16 @@ describe("ItemDetailsSectionComponent", () => { component.config.allowPersonalOwnership = true; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { - id: "col1", - name: "Collection 1", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, + createMockCollection("col1", "Collection 1", "org1") as CollectionView, ]; - component.originalCipherView = { + + getInitialCipherView.mockReturnValueOnce({ name: "cipher1", organizationId: "org1", folderId: "folder1", collectionIds: ["col1"], favorite: true, - } as CipherView; + }); await component.ngOnInit(); tick(); @@ -118,55 +143,6 @@ describe("ItemDetailsSectionComponent", () => { expect(updatedCipher.favorite).toBe(true); })); - it("should prioritize initialValues when editing an existing cipher ", fakeAsync(async () => { - component.config.allowPersonalOwnership = true; - component.config.organizations = [{ id: "org1" } as Organization]; - component.config.collections = [ - { - id: "col1", - name: "Collection 1", - organizationId: "org1", - assigned: true, - readOnly: true, - } as CollectionView, - { - id: "col2", - name: "Collection 2", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, - ]; - component.originalCipherView = { - name: "cipher1", - organizationId: "org1", - folderId: "folder1", - collectionIds: ["col1"], - favorite: true, - } as CipherView; - - component.config.initialValues = { - name: "new-name", - folderId: "new-folder", - organizationId: "bad-org" as OrganizationId, // Should not be set in edit mode - collectionIds: ["col2" as CollectionId], - }; - - await component.ngOnInit(); - tick(); - - expect(cipherFormProvider.patchCipher).toHaveBeenCalled(); - const patchFn = cipherFormProvider.patchCipher.mock.lastCall[0]; - - const updatedCipher = patchFn(new CipherView()); - - expect(updatedCipher.name).toBe("new-name"); - expect(updatedCipher.organizationId).toBe("org1"); - expect(updatedCipher.folderId).toBe("new-folder"); - expect(updatedCipher.collectionIds).toEqual(["col2"]); - expect(updatedCipher.favorite).toBe(true); - })); - it("should disable organizationId control if ownership change is not allowed", async () => { component.config.allowPersonalOwnership = false; component.config.organizations = [{ id: "org1" } as Organization]; @@ -198,8 +174,11 @@ describe("ItemDetailsSectionComponent", () => { }); describe("allowOwnershipChange", () => { - it("should not allow ownership change in edit mode", () => { + it("should not allow ownership change if in edit mode and the cipher is owned by an organization", () => { component.config.mode = "edit"; + component.originalCipherView = { + organizationId: "org1", + } as CipherView; expect(component.allowOwnershipChange).toBe(false); }); @@ -236,6 +215,7 @@ describe("ItemDetailsSectionComponent", () => { it("should show personal ownership when the configuration allows", () => { component.config.mode = "edit"; component.config.allowPersonalOwnership = true; + component.originalCipherView = {} as CipherView; component.config.organizations = [{ id: "134-433-22" } as Organization]; fixture.detectChanges(); @@ -249,6 +229,7 @@ describe("ItemDetailsSectionComponent", () => { it("should show personal ownership when the control is disabled", async () => { component.config.mode = "edit"; component.config.allowPersonalOwnership = false; + component.originalCipherView = {} as CipherView; component.config.organizations = [{ id: "134-433-22" } as Organization]; await component.ngOnInit(); fixture.detectChanges(); @@ -294,10 +275,13 @@ describe("ItemDetailsSectionComponent", () => { }); describe("cloneMode", () => { - it("should append '- Clone' to the title if in clone mode", async () => { + beforeEach(() => { component.config.mode = "clone"; + }); + + it("should append '- Clone' to the title if in clone mode", async () => { component.config.allowPersonalOwnership = true; - component.originalCipherView = { + const cipher = { name: "cipher1", organizationId: null, folderId: null, @@ -305,6 +289,8 @@ describe("ItemDetailsSectionComponent", () => { favorite: false, } as CipherView; + getInitialCipherView.mockReturnValueOnce(cipher); + i18nService.t.calledWith("clone").mockReturnValue("Clone"); await component.ngOnInit(); @@ -312,8 +298,28 @@ describe("ItemDetailsSectionComponent", () => { expect(component.itemDetailsForm.controls.name.value).toBe("cipher1 - Clone"); }); + it("does not append clone when the cipher was populated from the cache", async () => { + component.config.allowPersonalOwnership = true; + const cipher = { + name: "from cache cipher", + organizationId: null, + folderId: null, + collectionIds: null, + favorite: false, + } as CipherView; + + getInitialCipherView.mockReturnValueOnce(cipher); + + initializedWithCachedCipher.mockReturnValueOnce(true); + + i18nService.t.calledWith("clone").mockReturnValue("Clone"); + + await component.ngOnInit(); + + expect(component.itemDetailsForm.controls.name.value).toBe("from cache cipher"); + }); + it("should select the first organization if personal ownership is not allowed", async () => { - component.config.mode = "clone"; component.config.allowPersonalOwnership = false; component.config.organizations = [ { id: "org1" } as Organization, @@ -354,8 +360,8 @@ describe("ItemDetailsSectionComponent", () => { component.config.allowPersonalOwnership = true; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { id: "col1", name: "Collection 1", organizationId: "org1" } as CollectionView, - { id: "col2", name: "Collection 2", organizationId: "org1" } as CollectionView, + createMockCollection("col1", "Collection 1", "org1") as CollectionView, + createMockCollection("col2", "Collection 2", "org1") as CollectionView, ]; fixture.detectChanges(); @@ -376,36 +382,18 @@ describe("ItemDetailsSectionComponent", () => { it("should set collectionIds to originalCipher collections on first load", async () => { component.config.mode = "clone"; - component.originalCipherView = { + getInitialCipherView.mockReturnValueOnce({ name: "cipher1", organizationId: "org1", folderId: "folder1", collectionIds: ["col1", "col2"], favorite: true, - } as CipherView; + }); component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { - id: "col1", - name: "Collection 1", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, - { - id: "col2", - name: "Collection 2", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, - { - id: "col3", - name: "Collection 3", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, + createMockCollection("col1", "Collection 1", "org1") as CollectionView, + createMockCollection("col2", "Collection 2", "org1") as CollectionView, + createMockCollection("col3", "Collection 3", "org1") as CollectionView, ]; fixture.detectChanges(); @@ -423,13 +411,7 @@ describe("ItemDetailsSectionComponent", () => { component.config.allowPersonalOwnership = true; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { - id: "col1", - name: "Collection 1", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, + createMockCollection("col1", "Collection 1", "org1") as CollectionView, ]; fixture.detectChanges(); @@ -447,6 +429,13 @@ describe("ItemDetailsSectionComponent", () => { it("should show readonly hint if readonly collections are present", async () => { component.config.mode = "edit"; + getInitialCipherView.mockReturnValueOnce({ + name: "cipher1", + organizationId: "org1", + folderId: "folder1", + collectionIds: ["col1", "col2", "col3"], + favorite: true, + }); component.originalCipherView = { name: "cipher1", organizationId: "org1", @@ -456,27 +445,9 @@ describe("ItemDetailsSectionComponent", () => { } as CipherView; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { - id: "col1", - name: "Collection 1", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, - { - id: "col2", - name: "Collection 2", - organizationId: "org1", - assigned: true, - readOnly: false, - } as CollectionView, - { - id: "col3", - name: "Collection 3", - organizationId: "org1", - readOnly: true, - assigned: true, - } as CollectionView, + createMockCollection("col1", "Collection 1", "org1", true, false) as CollectionView, + createMockCollection("col2", "Collection 2", "org1", true, false) as CollectionView, + createMockCollection("col3", "Collection 3", "org1", true) as CollectionView, ]; await component.ngOnInit(); @@ -494,27 +465,9 @@ describe("ItemDetailsSectionComponent", () => { component.config.allowPersonalOwnership = true; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { - id: "col1", - name: "Collection 1", - organizationId: "org1", - readOnly: true, - assigned: false, - } as CollectionView, - { - id: "col2", - name: "Collection 2", - organizationId: "org1", - readOnly: true, - assigned: false, - } as CollectionView, - { - id: "col3", - name: "Collection 3", - organizationId: "org1", - readOnly: false, - assigned: true, - } as CollectionView, + createMockCollection("col1", "Collection 1", "org1", true, false) as CollectionView, + createMockCollection("col2", "Collection 2", "org1", true, false) as CollectionView, + createMockCollection("col3", "Collection 3", "org1", false, false) as CollectionView, ]; fixture.detectChanges(); @@ -531,26 +484,9 @@ describe("ItemDetailsSectionComponent", () => { component.config.mode = "edit"; component.config.admin = true; component.config.collections = [ - { - id: "col1", - name: "Collection 1", - organizationId: "org1", - readOnly: true, - assigned: false, - } as CollectionView, - { - id: "col2", - name: "Collection 2", - organizationId: "org1", - assigned: false, - } as CollectionView, - { - id: "col3", - name: "Collection 3", - organizationId: "org1", - readOnly: true, - assigned: false, - } as CollectionView, + createMockCollection("col1", "Collection 1", "org1", true, false) as CollectionView, + createMockCollection("col2", "Collection 2", "org1", false, true) as CollectionView, + createMockCollection("col3", "Collection 3", "org1", true, false) as CollectionView, ]; component.originalCipherView = { name: "cipher1", @@ -559,10 +495,14 @@ describe("ItemDetailsSectionComponent", () => { collectionIds: ["col1", "col2", "col3"], favorite: true, } as CipherView; + + getInitialCipherView.mockReturnValue(component.originalCipherView); + component.config.organizations = [{ id: "org1" } as Organization]; }); it("should not show collections as readonly when `config.admin` is true", async () => { + component.config.isAdminConsole = true; await component.ngOnInit(); fixture.detectChanges(); @@ -574,8 +514,7 @@ describe("ItemDetailsSectionComponent", () => { await component.ngOnInit(); fixture.detectChanges(); - - expect(component["readOnlyCollections"]).toEqual(["Collection 1", "Collection 3"]); + expect(component["readOnlyCollectionsNames"]).toEqual(["Collection 1", "Collection 3"]); }); }); }); diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index f7fd228232e4..85cd85bbf037 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CommonModule, NgClass } from "@angular/common"; +import { CommonModule } from "@angular/common"; import { Component, DestroyRef, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; @@ -8,6 +8,7 @@ import { concatMap, map } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -43,7 +44,6 @@ import { CipherFormContainer } from "../../cipher-form-container"; SelectModule, SectionHeaderComponent, IconButtonModule, - NgClass, JslibModule, CommonModule, ], @@ -67,7 +67,7 @@ export class ItemDetailsSectionComponent implements OnInit { * Collections that are already assigned to the cipher and are read-only. These cannot be removed. * @protected */ - protected readOnlyCollections: string[] = []; + protected readOnlyCollections: CollectionView[] = []; protected showCollectionsControl: boolean; @@ -79,6 +79,10 @@ export class ItemDetailsSectionComponent implements OnInit { @Input() originalCipherView: CipherView; + + get readOnlyCollectionsNames(): string[] { + return this.readOnlyCollections.map((c) => c.name); + } /** * Whether the form is in partial edit mode. Only the folder and favorite controls are available. */ @@ -133,7 +137,10 @@ export class ItemDetailsSectionComponent implements OnInit { name: value.name, organizationId: value.organizationId, folderId: value.folderId, - collectionIds: value.collectionIds?.map((c) => c.id) || [], + collectionIds: [ + ...(value.collectionIds?.map((c) => c.id) || []), + ...this.readOnlyCollections.map((c) => c.id), + ], favorite: value.favorite, } as CipherView); return cipher; @@ -150,8 +157,8 @@ export class ItemDetailsSectionComponent implements OnInit { } get allowOwnershipChange() { - // Do not allow ownership change in edit mode. - if (this.config.mode === "edit") { + // Do not allow ownership change in edit mode and the cipher is owned by an organization + if (this.config.mode === "edit" && this.originalCipherView?.organizationId != null) { return false; } @@ -183,8 +190,10 @@ export class ItemDetailsSectionComponent implements OnInit { throw new Error("No organizations available for ownership."); } - if (this.originalCipherView) { - await this.initFromExistingCipher(); + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { + await this.initFromExistingCipher(prefillCipher); } else { this.itemDetailsForm.setValue({ name: this.initialValues?.name || "", @@ -210,45 +219,70 @@ export class ItemDetailsSectionComponent implements OnInit { .subscribe(); } - private async initFromExistingCipher() { + private async initFromExistingCipher(prefillCipher: CipherView) { + const { name, folderId, collectionIds } = prefillCipher; + this.itemDetailsForm.setValue({ - name: this.initialValues?.name ?? this.originalCipherView.name, - organizationId: this.originalCipherView.organizationId, // We do not allow changing ownership of an existing cipher. - folderId: this.initialValues?.folderId ?? this.originalCipherView.folderId, + name: name ? name : (this.initialValues?.name ?? ""), + organizationId: prefillCipher.organizationId, // We do not allow changing ownership of an existing cipher. + folderId: folderId ? folderId : (this.initialValues?.folderId ?? null), collectionIds: [], - favorite: this.originalCipherView.favorite, + favorite: prefillCipher.favorite, }); + const orgId = this.itemDetailsForm.controls.organizationId.value as OrganizationId; + const organization = this.organizations.find((o) => o.id === orgId); + const initializedWithCachedCipher = this.cipherFormContainer.initializedWithCachedCipher(); + // Configure form for clone mode. if (this.config.mode === "clone") { - this.itemDetailsForm.controls.name.setValue( - this.originalCipherView.name + " - " + this.i18nService.t("clone"), - ); + if (!initializedWithCachedCipher) { + this.itemDetailsForm.controls.name.setValue( + prefillCipher.name + " - " + this.i18nService.t("clone"), + ); + } - if (!this.allowPersonalOwnership && this.originalCipherView.organizationId == null) { + if (!this.allowPersonalOwnership && prefillCipher.organizationId == null) { this.itemDetailsForm.controls.organizationId.setValue(this.defaultOwner); } } - await this.updateCollectionOptions( - this.initialValues?.collectionIds ?? - (this.originalCipherView.collectionIds as CollectionId[]), - ); + const prefillCollections = collectionIds?.length + ? (collectionIds as CollectionId[]) + : (this.initialValues?.collectionIds ?? []); + + await this.updateCollectionOptions(prefillCollections); + + if (!organization?.canEditAllCiphers && !prefillCipher.canAssignToCollections) { + this.itemDetailsForm.controls.collectionIds.disable(); + } if (this.partialEdit) { this.itemDetailsForm.disable(); this.itemDetailsForm.controls.favorite.enable(); this.itemDetailsForm.controls.folderId.enable(); } else if (this.config.mode === "edit") { - this.readOnlyCollections = this.collections - .filter( + if (!this.config.isAdminConsole || !this.config.admin) { + this.readOnlyCollections = this.collections.filter( // When the configuration is set up for admins, they can alter read only collections (c) => + c.organizationId === orgId && c.readOnly && - !this.config.admin && this.originalCipherView.collectionIds.includes(c.id as CollectionId), - ) - .map((c) => c.name); + ); + + // When Owners/Admins access setting is turned on. + // Disable Collections Options if Owner/Admin does not have Edit/Manage permissions on item + // Disable Collections Options if Custom user does not have Edit/Manage permissions on item + if ( + (organization.allowAdminAccessToAllCollectionItems && + (!this.originalCipherView.viewPassword || !this.originalCipherView.edit)) || + (organization.type === OrganizationUserType.Custom && + !this.originalCipherView.viewPassword) + ) { + this.itemDetailsForm.controls.collectionIds.disable(); + } + } } } diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index 69cfd4197423..199354124ff0 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -13,10 +13,7 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { ToastService } from "@bitwarden/components"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive"; +import { BitPasswordInputToggleDirective, ToastService } from "@bitwarden/components"; import { CipherFormGenerationService } from "../../abstractions/cipher-form-generation.service"; import { TotpCaptureService } from "../../abstractions/totp-capture.service"; @@ -45,9 +42,11 @@ describe("LoginDetailsSectionComponent", () => { let configService: MockProxy; const collect = jest.fn().mockResolvedValue(null); + const getInitialCipherView = jest.fn(() => null); beforeEach(async () => { - cipherFormContainer = mock(); + getInitialCipherView.mockClear(); + cipherFormContainer = mock({ getInitialCipherView }); generationService = mock(); auditService = mock(); @@ -122,18 +121,18 @@ describe("LoginDetailsSectionComponent", () => { }); it("initializes 'loginDetailsForm' with original cipher view values", async () => { - (cipherFormContainer.originalCipherView as CipherView) = { + getInitialCipherView.mockReturnValueOnce({ viewPassword: true, login: { password: "original-password", username: "original-username", totp: "original-totp", - } as LoginView, - } as CipherView; + }, + }); - await component.ngOnInit(); + component.ngOnInit(); - expect(component.loginDetailsForm.value).toEqual({ + expect(component.loginDetailsForm.getRawValue()).toEqual({ username: "original-username", password: "original-password", totp: "original-totp", @@ -141,22 +140,23 @@ describe("LoginDetailsSectionComponent", () => { }); it("initializes 'loginDetailsForm' with initialValues that override any original cipher view values", async () => { - (cipherFormContainer.originalCipherView as CipherView) = { + getInitialCipherView.mockReturnValueOnce({ viewPassword: true, login: { password: "original-password", username: "original-username", totp: "original-totp", - } as LoginView, - } as CipherView; + }, + }); + cipherFormContainer.config.initialValues = { username: "new-username", password: "new-password", }; - await component.ngOnInit(); + component.ngOnInit(); - expect(component.loginDetailsForm.value).toEqual({ + expect(component.loginDetailsForm.getRawValue()).toEqual({ username: "new-username", password: "new-password", totp: "original-totp", @@ -165,12 +165,12 @@ describe("LoginDetailsSectionComponent", () => { describe("viewHiddenFields", () => { beforeEach(() => { - (cipherFormContainer.originalCipherView as CipherView) = { + getInitialCipherView.mockReturnValue({ viewPassword: false, login: { password: "original-password", - } as LoginView, - } as CipherView; + }, + }); }); it("returns value of originalCipher.viewPassword", () => { @@ -251,6 +251,10 @@ describe("LoginDetailsSectionComponent", () => { }); describe("password", () => { + beforeEach(() => { + getInitialCipherView.mockReturnValue(null); + }); + const getGeneratePasswordBtn = () => fixture.nativeElement.querySelector("button[data-testid='generate-password-button']"); @@ -520,11 +524,11 @@ describe("LoginDetailsSectionComponent", () => { fixture.nativeElement.querySelector("input[data-testid='passkey-field']"); beforeEach(() => { - (cipherFormContainer.originalCipherView as CipherView) = { + getInitialCipherView.mockReturnValue({ login: Object.assign(new LoginView(), { fido2Credentials: [{ creationDate: passkeyDate } as Fido2CredentialView], }), - } as CipherView; + }); fixture = TestBed.createComponent(LoginDetailsSectionComponent); component = fixture.componentInstance; @@ -567,7 +571,11 @@ describe("LoginDetailsSectionComponent", () => { }); it("hides the passkey field when missing a passkey", () => { - (cipherFormContainer.originalCipherView as CipherView).login.fido2Credentials = []; + getInitialCipherView.mockReturnValueOnce({ + login: Object.assign(new LoginView(), { + fido2Credentials: [], + }), + }); fixture.detectChanges(); diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index 2296932aca36..92def7058528 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -139,11 +139,13 @@ export class LoginDetailsSectionComponent implements OnInit { }); } - async ngOnInit() { - if (this.cipherFormContainer.originalCipherView?.login) { - this.initFromExistingCipher(this.cipherFormContainer.originalCipherView.login); + ngOnInit() { + const prefillCipher = this.cipherFormContainer.getInitialCipherView(); + + if (prefillCipher) { + this.initFromExistingCipher(prefillCipher.login); } else { - await this.initNewCipher(); + this.initNewCipher(); } if (this.cipherFormContainer.config.mode === "partial-edit") { @@ -166,7 +168,7 @@ export class LoginDetailsSectionComponent implements OnInit { } } - private async initNewCipher() { + private initNewCipher() { this.loginDetailsForm.patchValue({ username: this.initialValues?.username || "", password: this.initialValues?.password || "", diff --git a/libs/vault/src/cipher-form/index.ts b/libs/vault/src/cipher-form/index.ts index 8cb779a8ec34..0172733b682c 100644 --- a/libs/vault/src/cipher-form/index.ts +++ b/libs/vault/src/cipher-form/index.ts @@ -9,3 +9,5 @@ export { TotpCaptureService } from "./abstractions/totp-capture.service"; export { CipherFormGenerationService } from "./abstractions/cipher-form-generation.service"; export { DefaultCipherFormConfigService } from "./services/default-cipher-form-config.service"; export { CipherFormGeneratorComponent } from "./components/cipher-generator/cipher-form-generator.component"; +export { CipherFormContainer } from "../cipher-form/cipher-form-container"; +export { CipherFormComponent } from "./components/cipher-form.component"; diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts new file mode 100644 index 000000000000..6236e2d3dac5 --- /dev/null +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts @@ -0,0 +1,97 @@ +import { signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { CipherFormCacheService } from "./default-cipher-form-cache.service"; + +describe("CipherFormCacheService", () => { + let service: CipherFormCacheService; + let testBed: TestBed; + const cacheSignal = signal(null); + const getCacheSignal = jest.fn().mockReturnValue(cacheSignal); + const getFeatureFlag = jest.fn().mockResolvedValue(false); + const cacheSetMock = jest.spyOn(cacheSignal, "set"); + + beforeEach(() => { + getCacheSignal.mockClear(); + getFeatureFlag.mockClear(); + cacheSetMock.mockClear(); + + testBed = TestBed.configureTestingModule({ + providers: [ + { provide: ViewCacheService, useValue: { signal: getCacheSignal } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + CipherFormCacheService, + ], + }); + }); + + describe("feature enabled", () => { + beforeEach(async () => { + getFeatureFlag.mockResolvedValue(true); + }); + + it("`getCachedCipherView` returns the cipher", async () => { + cacheSignal.set({ id: "cipher-4" } as CipherView); + service = testBed.inject(CipherFormCacheService); + await service.init(); + + expect(service.getCachedCipherView()).toEqual({ id: "cipher-4" }); + }); + + it("updates the signal value", async () => { + service = testBed.inject(CipherFormCacheService); + await service.init(); + + service.cacheCipherView({ id: "cipher-5" } as CipherView); + + expect(cacheSignal.set).toHaveBeenCalledWith({ id: "cipher-5" }); + }); + + describe("initializedWithValue", () => { + it("sets `initializedWithValue` to true when there is a cached cipher", async () => { + cacheSignal.set({ id: "cipher-3" } as CipherView); + service = testBed.inject(CipherFormCacheService); + await service.init(); + + expect(service.initializedWithValue).toBe(true); + }); + + it("sets `initializedWithValue` to false when there is not a cached cipher", async () => { + cacheSignal.set(null); + service = testBed.inject(CipherFormCacheService); + await service.init(); + + expect(service.initializedWithValue).toBe(false); + }); + }); + }); + + describe("featured disabled", () => { + beforeEach(async () => { + cacheSignal.set({ id: "cipher-1" } as CipherView); + getFeatureFlag.mockResolvedValue(false); + cacheSetMock.mockClear(); + + service = testBed.inject(CipherFormCacheService); + await service.init(); + }); + + it("sets `initializedWithValue` to false", () => { + expect(service.initializedWithValue).toBe(false); + }); + + it("`getCachedCipherView` returns null", () => { + expect(service.getCachedCipherView()).toBeNull(); + }); + + it("does not update the signal value", () => { + service.cacheCipherView({ id: "cipher-2" } as CipherView); + + expect(cacheSignal.set).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts new file mode 100644 index 000000000000..268b2db306ba --- /dev/null +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts @@ -0,0 +1,73 @@ +import { inject, Injectable } from "@angular/core"; + +import { 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +const CIPHER_FORM_CACHE_KEY = "cipher-form-cache"; + +@Injectable() +export class CipherFormCacheService { + private viewCacheService: ViewCacheService = inject(ViewCacheService); + private configService: ConfigService = inject(ConfigService); + + /** True when the `PM9111ExtensionPersistAddEditForm` flag is enabled */ + private featureEnabled: boolean = false; + + /** + * When true the `CipherFormCacheService` a cipher was stored in cache when the service was initialized. + * Otherwise false, when the cache was empty. + * + * This is helpful to know the initial state of the cache as it can be populated quickly after initialization. + */ + initializedWithValue: boolean; + + private cipherCache = this.viewCacheService.signal({ + key: CIPHER_FORM_CACHE_KEY, + initialValue: null, + deserializer: CipherView.fromJSON, + }); + + constructor() { + this.initializedWithValue = !!this.cipherCache(); + } + + /** + * Must be called once before interacting with the cached cipher, otherwise methods will be noop. + */ + async init() { + this.featureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM9111ExtensionPersistAddEditForm, + ); + + if (!this.featureEnabled) { + this.initializedWithValue = false; + } + } + + /** + * Update the cache with the new CipherView. + */ + cacheCipherView(cipherView: CipherView): void { + if (!this.featureEnabled) { + return; + } + + // Create a new shallow reference to force the signal to update + // By default, signals use `Object.is` to determine equality + // Docs: https://angular.dev/guide/signals#signal-equality-functions + this.cipherCache.set({ ...cipherView } as CipherView); + } + + /** + * Returns the cached CipherView when available. + */ + getCachedCipherView(): CipherView | null { + if (!this.featureEnabled) { + return null; + } + + return this.cipherCache(); + } +} diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index 93a53345d3ae..28b13b51c61a 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -8,7 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -46,7 +46,7 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { const [organizations, collections, allowPersonalOwnership, folders, cipher] = await firstValueFrom( combineLatest([ - this.organizations$, + this.organizations$(activeUserId), this.collectionService.encryptedCollections$.pipe( switchMap((c) => this.collectionService.decryptedCollections$.pipe( @@ -78,13 +78,17 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { }; } - private organizations$ = this.organizationService.organizations$.pipe( - map((orgs) => - orgs.filter( - (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, - ), - ), - ); + organizations$(userId: UserId) { + return this.organizationService + .organizations$(userId) + .pipe( + map((orgs) => + orgs.filter( + (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, + ), + ), + ); + } private allowPersonalOwnership$ = this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 92a28f9b15f3..059214cc1854 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -63,8 +63,16 @@ export class DefaultCipherFormService implements CipherFormService { const originalCollectionIds = new Set(config.originalCipher.collectionIds ?? []); const newCollectionIds = new Set(cipher.collectionIds ?? []); - // If the collectionIds are the same, update the cipher normally - if (isSetEqual(originalCollectionIds, newCollectionIds)) { + // Call shareWithServer if the owner is changing from a user to an organization + if (config.originalCipher.organizationId === null && cipher.organizationId != null) { + savedCipher = await this.cipherService.shareWithServer( + cipher, + cipher.organizationId, + cipher.collectionIds, + activeUserId, + ); + // If the collectionIds are the same, update the cipher normally + } else if (isSetEqual(originalCollectionIds, newCollectionIds)) { savedCipher = await this.cipherService.updateWithServer(encryptedCipher, config.admin); } else { // Updating a cipher with collection changes is not supported with a single request currently diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index f872ad0cf156..425afeace845 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -4,9 +4,13 @@ import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; import { CollectionId } from "@bitwarden/common/types/guid"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -133,9 +137,12 @@ export class CipherViewComponent implements OnChanges, OnDestroy { ); } - if (this.cipher.organizationId) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (this.cipher.organizationId && userId) { this.organization$ = this.organizationService - .get$(this.cipher.organizationId) + .organizations$(userId) + .pipe(getOrganizationById(this.cipher.organizationId)) .pipe(takeUntil(this.destroyed$)); } diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html index 45ddc3c1dead..ab31ede57bb3 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html @@ -38,6 +38,7 @@

{{ "customFields" | i18n }}

type="button" bitIconButton bitPasswordInputToggle + *ngIf="canViewPassword" (toggledChange)="logHiddenEvent($event)" > diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts index b41b351e1975..313607ce3a6c 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts @@ -59,6 +59,10 @@ export class CustomFieldV2Component implements OnInit { return this.i18nService.t(linkedType.i18nKey); } + get canViewPassword() { + return this.cipher.viewPassword; + } + async logHiddenEvent(hiddenFieldVisible: boolean) { if (hiddenFieldVisible) { await this.eventCollectionService.collect( diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.html b/libs/vault/src/cipher-view/item-history/item-history-v2.component.html index a207b8fa1caf..19d1cfe17442 100644 --- a/libs/vault/src/cipher-view/item-history/item-history-v2.component.html +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.html @@ -10,15 +10,15 @@

{{ "itemHistory" | i18n }}

{{ "dateCreated" | i18n }}: {{ cipher.creationDate | date: "medium" }}

@@ -26,7 +26,7 @@

{{ "itemHistory" | i18n }}

{{ cipher.passwordRevisionDisplayDate | date: "medium" }}

diff --git a/libs/vault/src/components/carousel/carousel-button/carousel-button.component.spec.ts b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.spec.ts new file mode 100644 index 000000000000..91239cd75202 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.spec.ts @@ -0,0 +1,71 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { VaultCarouselSlideComponent } from "../carousel-slide/carousel-slide.component"; + +import { VaultCarouselButtonComponent } from "./carousel-button.component"; + +describe("VaultCarouselButtonComponent", () => { + let fixture: ComponentFixture; + let component: VaultCarouselButtonComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselButtonComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VaultCarouselButtonComponent); + component = fixture.componentInstance; + component.slide = { label: "Test Label" } as VaultCarouselSlideComponent; + + fixture.detectChanges(); + }); + + it("emits click event", () => { + jest.spyOn(component.onClick, "emit"); + component.button.nativeElement.click(); + + expect(component.onClick.emit).toHaveBeenCalled(); + }); + + it("focuses on button", () => { + component.focus(); + + expect(document.activeElement).toBe(component.button.nativeElement); + }); + + it('sets the "aria-label" attribute', () => { + expect(component.button.nativeElement.getAttribute("aria-label")).toBe("Test Label"); + }); + + describe("is active", () => { + beforeEach(() => { + component.isActive = true; + fixture.detectChanges(); + }); + + it("sets the aria-selected to true", () => { + expect(component.button.nativeElement.getAttribute("aria-selected")).toBe("true"); + }); + + it("adds button to tab index", () => { + expect(component.button.nativeElement.getAttribute("tabindex")).toBe("0"); + }); + }); + + describe("is not active", () => { + beforeEach(() => { + component.isActive = false; + fixture.detectChanges(); + }); + + it("sets the aria-selected to false", () => { + expect(component.button.nativeElement.getAttribute("aria-selected")).toBe("false"); + }); + + it("removes button from tab index", () => { + expect(component.button.nativeElement.getAttribute("tabindex")).toBe("-1"); + }); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts new file mode 100644 index 000000000000..d0e353dcc76b --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts @@ -0,0 +1,45 @@ +import { FocusableOption } from "@angular/cdk/a11y"; +import { CommonModule } from "@angular/common"; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from "@angular/core"; + +import { IconModule } from "@bitwarden/components"; + +import { CarouselIcon } from "../carousel-icons/carousel-icon"; +import { VaultCarouselSlideComponent } from "../carousel-slide/carousel-slide.component"; + +@Component({ + selector: "vault-carousel-button", + templateUrl: "carousel-button.component.html", + standalone: true, + imports: [CommonModule, IconModule], +}) +export class VaultCarouselButtonComponent implements FocusableOption { + /** Slide component that is associated with the individual button */ + @Input({ required: true }) slide!: VaultCarouselSlideComponent; + + @ViewChild("btn", { static: true }) button!: ElementRef; + protected CarouselIcon = CarouselIcon; + + /** When set to true the button is shown in an active state. */ + @Input({ required: true }) isActive!: boolean; + + /** Emits when the button is clicked. */ + @Output() onClick = new EventEmitter(); + + /** Focuses the underlying button element. */ + focus(): void { + this.button.nativeElement.focus(); + } + + protected get dynamicClasses(): string[] { + const activeClasses = ["[&_rect]:tw-fill-primary-600", "tw-text-primary-600"]; + + const inactiveClasses = [ + "tw-text-muted", + "[&_rect]:hover:tw-fill-text-muted", + "focus-visible:tw-text-info-700", + ]; + + return this.isActive ? activeClasses : inactiveClasses; + } +} diff --git a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.html b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.html new file mode 100644 index 000000000000..4d97cfd054f7 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts new file mode 100644 index 000000000000..2900225b8867 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts @@ -0,0 +1,58 @@ +import { TemplatePortal } from "@angular/cdk/portal"; +import { Component, OnInit, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { VaultCarouselContentComponent } from "./carousel-content.component"; + +@Component({ + selector: "app-test-template-ref", + standalone: true, + imports: [VaultCarouselContentComponent], + template: ` + +

Test Template Content

+
+ + `, +}) +class TestTemplateRefComponent implements OnInit { + // Test template content by creating a wrapping component and then pass a portal to the carousel content component. + @ViewChild("template", { static: true }) template!: TemplateRef; + portal!: TemplatePortal; + + constructor(private viewContainerRef: ViewContainerRef) {} + + ngOnInit() { + this.portal = new TemplatePortal(this.template, this.viewContainerRef); + } +} + +describe("VaultCarouselContentComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselContentComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestTemplateRefComponent); + fixture.detectChanges(); + }); + + it("displays content", () => { + const carouselContent = fixture.debugElement.query(By.directive(VaultCarouselContentComponent)); + + expect(carouselContent.nativeElement.textContent).toBe("Test Template Content"); + }); + + it("sets aria attributes for screen readers", () => { + const carouselContent = fixture.debugElement.query(By.directive(VaultCarouselContentComponent)); + const wrappingDiv = carouselContent.nativeElement.querySelector("div"); + + expect(wrappingDiv.getAttribute("aria-live")).toBe("polite"); + expect(wrappingDiv.getAttribute("aria-atomic")).toBe("false"); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts new file mode 100644 index 000000000000..7051a8ff8fab --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts @@ -0,0 +1,13 @@ +import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal"; +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "vault-carousel-content", + templateUrl: "carousel-content.component.html", + standalone: true, + imports: [CdkPortalOutlet], +}) +export class VaultCarouselContentComponent { + /** Content to be displayed for the carousel. */ + @Input({ required: true }) content!: TemplatePortal; +} diff --git a/libs/vault/src/components/carousel/carousel-icons/carousel-icon.ts b/libs/vault/src/components/carousel/carousel-icons/carousel-icon.ts new file mode 100644 index 000000000000..64052e6fa6c8 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-icons/carousel-icon.ts @@ -0,0 +1,7 @@ +import { svgIcon } from "@bitwarden/components"; + +export const CarouselIcon = svgIcon` + + + +`; diff --git a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.html b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.html new file mode 100644 index 000000000000..0c5c6f1cd701 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.html @@ -0,0 +1,9 @@ + +
+ +
+
diff --git a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts new file mode 100644 index 000000000000..e69a2a2d7580 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts @@ -0,0 +1,40 @@ +import { TemplatePortal } from "@angular/cdk/portal"; +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { VaultCarouselSlideComponent } from "./carousel-slide.component"; + +@Component({ + selector: "app-test-carousel-slide", + standalone: true, + imports: [VaultCarouselSlideComponent], + template: `

Carousel Slide Content!

`, +}) +class TestCarouselSlideComponent { + // Test template content by creating a wrapping component. +} + +describe("VaultCarouselSlideComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselSlideComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestCarouselSlideComponent); + fixture.detectChanges(); + }); + + it("sets content", () => { + const slideComponent = fixture.debugElement.query( + By.directive(VaultCarouselSlideComponent), + ).componentInstance; + + expect(slideComponent.content).not.toBeNull(); + expect(slideComponent.content).toBeInstanceOf(TemplatePortal); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts new file mode 100644 index 000000000000..23ce6a3d7db8 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts @@ -0,0 +1,41 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { Component, Input, OnInit, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; + +@Component({ + selector: "vault-carousel-slide", + templateUrl: "./carousel-slide.component.html", + standalone: true, +}) +export class VaultCarouselSlideComponent implements OnInit { + /** `aria-label` that is assigned to the carousel toggle. */ + @Input({ required: true }) label!: string; + + /** + * Should be set to true when the slide has no focusable elements. + * + * When the slide does not contain any focusable elements or the first element with content is not focusable, + * this should be set to 0 to include it in the tab sequence of the page. + * + * @remarks See note 4 of https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/ + */ + @Input({ transform: coerceBooleanProperty }) noFocusableChildren?: true; + + @ViewChild(TemplateRef, { static: true }) implicitContent!: TemplateRef; + + private _contentPortal: TemplatePortal | null = null; + + /** + * A Portal containing the content of the slide. + * Used by `VaultCarouselComponent` when the slide becomes active. + */ + get content(): TemplatePortal | null { + return this._contentPortal; + } + + constructor(private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + this._contentPortal = new TemplatePortal(this.implicitContent, this.viewContainerRef); + } +} diff --git a/libs/vault/src/components/carousel/carousel.component.html b/libs/vault/src/components/carousel/carousel.component.html new file mode 100644 index 000000000000..b3e124e02bbc --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.component.html @@ -0,0 +1,25 @@ +
+ +
+ +
+
+ +
+
diff --git a/libs/vault/src/components/carousel/carousel.component.spec.ts b/libs/vault/src/components/carousel/carousel.component.spec.ts new file mode 100644 index 000000000000..500b8115bad8 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.component.spec.ts @@ -0,0 +1,73 @@ +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; +import { VaultCarouselComponent } from "./carousel.component"; + +@Component({ + selector: "app-test-carousel-slide", + standalone: true, + imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + template: ` + + +

First Carousel Heading

+
+ +

Second Carousel Heading

+
+ +

Third Carousel Heading

+
+
+ `, +}) +class TestCarouselComponent { + // Test carousel by creating a wrapping component. +} + +describe("VaultCarouselComponent", () => { + let fixture: ComponentFixture; + let component: VaultCarouselComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestCarouselComponent); + fixture.detectChanges(); + component = fixture.debugElement.query(By.directive(VaultCarouselComponent)).componentInstance; + }); + + it("sets first slide as active by default", () => { + expect(component["selectedIndex"]).toBe(0); + }); + + it("shows the active slides content", () => { + // Set the second slide as active + fixture.debugElement.queryAll(By.css("button"))[1].nativeElement.click(); + fixture.detectChanges(); + + const heading = fixture.debugElement.query(By.css("h1")).nativeElement; + + expect(heading.textContent).toBe("Second Carousel Heading"); + }); + + it("sets the initial focused button as the first button", () => { + expect(component["keyManager"]?.activeItemIndex).toBe(0); + }); + + it('emits "slideChange" event when slide changes', () => { + jest.spyOn(component.slideChange, "emit"); + + const thirdSlideButton = fixture.debugElement.queryAll(By.css("button"))[2]; + + thirdSlideButton.nativeElement.click(); + + expect(component.slideChange.emit).toHaveBeenCalledWith(2); + }); +}); diff --git a/libs/vault/src/components/carousel/carousel.component.ts b/libs/vault/src/components/carousel/carousel.component.ts new file mode 100644 index 000000000000..ab6d0a38f3e7 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.component.ts @@ -0,0 +1,145 @@ +import { FocusKeyManager } from "@angular/cdk/a11y"; +import { CdkPortalOutlet } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + Input, + Output, + QueryList, + ViewChild, + ViewChildren, + inject, +} from "@angular/core"; + +import { ButtonModule } from "@bitwarden/components"; + +import { VaultCarouselButtonComponent } from "./carousel-button/carousel-button.component"; +import { VaultCarouselContentComponent } from "./carousel-content/carousel-content.component"; +import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; + +@Component({ + selector: "vault-carousel", + templateUrl: "./carousel.component.html", + standalone: true, + imports: [ + CdkPortalOutlet, + CommonModule, + ButtonModule, + VaultCarouselContentComponent, + VaultCarouselButtonComponent, + ], +}) +export class VaultCarouselComponent implements AfterViewInit { + private changeDetectorRef = inject(ChangeDetectorRef); + /** + * Accessible Label for the carousel + * + * @remarks + * The label should not include the word "carousel", `aria-roledescription="carousel"` is already included. + */ + @Input({ required: true }) label = ""; + + /** + * Emits the index of of the newly selected slide. + */ + @Output() slideChange = new EventEmitter(); + + /** All slides within the carousel. */ + @ContentChildren(VaultCarouselSlideComponent) slides!: QueryList; + + /** All buttons that control the carousel */ + @ViewChildren(VaultCarouselButtonComponent) + carouselButtons!: QueryList; + + /** Wrapping container for the carousel content and buttons */ + @ViewChild("container") carouselContainer!: ElementRef; + + /** Container for the carousel buttons */ + @ViewChild("carouselButtonWrapper") carouselButtonWrapper!: ElementRef; + + /** Temporary container containing `tempSlideOutlet` */ + @ViewChild("tempSlideContainer") tempSlideContainer!: ElementRef; + + /** Outlet to temporary render each slide within */ + @ViewChild(CdkPortalOutlet) tempSlideOutlet!: CdkPortalOutlet; + + /** The currently selected index of the carousel. */ + protected selectedIndex = 0; + + /** + * Slides that have differing heights can cause the carousel controls to jump. + * Set the min height based on the tallest slide. + */ + protected minHeight: `${number}px` | null = null; + + /** + * Focus key manager for keeping tab controls accessible. + * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions + */ + protected keyManager: FocusKeyManager | null = null; + + /** Set the selected index of the carousel. */ + protected selectSlide(index: number) { + this.selectedIndex = index; + this.slideChange.emit(index); + } + + ngAfterViewInit(): void { + this.keyManager = new FocusKeyManager(this.carouselButtons) + .withHorizontalOrientation("ltr") + .withWrap() + .withHomeAndEnd(); + + // Set the first carousel button as active, this avoids having to double tab the arrow keys on initial focus. + this.keyManager.setFirstItemActive(); + + this.setMinHeightOfCarousel(); + } + + /** + * Slides of differing height can cause the carousel to jump in height. + * Render each slide in a temporary portal outlet to get the height of each slide + * and store the tallest slide height. + */ + private setMinHeightOfCarousel() { + // Store the height of the carousel button element. + const heightOfButtonsPx = this.carouselButtonWrapper.nativeElement.offsetHeight; + + // Get the width of the carousel so we know how much space each slide can render within. + const containerWidth = this.carouselContainer.nativeElement.offsetWidth; + const containerHeight = this.carouselContainer.nativeElement.offsetHeight; + + // Set the width of the temp container to render each slide inside of. + this.tempSlideContainer.nativeElement.style.width = `${containerWidth}px`; + + // The first slide is already rendered at this point, use the height of the container + // to determine the height of the first slide. + let tallestSlideHeightPx = containerHeight - heightOfButtonsPx; + + this.slides.forEach((slide, index) => { + // Skip the first slide, the height is accounted for above. + if (index === this.selectedIndex) { + return; + } + + this.tempSlideOutlet.attach(slide.content); + + // Store the height of the current slide if is larger than the current stored height; + if (this.tempSlideContainer.nativeElement.offsetHeight > tallestSlideHeightPx) { + tallestSlideHeightPx = this.tempSlideContainer.nativeElement.offsetHeight; + } + + // cleanup the outlet + this.tempSlideOutlet.detach(); + }); + + // Set the min height of the entire carousel based on the largest slide. + this.minHeight = `${tallestSlideHeightPx + heightOfButtonsPx}px`; + this.changeDetectorRef.detectChanges(); + } +} diff --git a/libs/vault/src/components/carousel/carousel.stories.ts b/libs/vault/src/components/carousel/carousel.stories.ts new file mode 100644 index 000000000000..521a561a19f4 --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.stories.ts @@ -0,0 +1,76 @@ +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; + +import { ButtonComponent, TypographyModule } from "@bitwarden/components"; + +import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; +import { VaultCarouselComponent } from "./carousel.component"; + +export default { + title: "Vault/Carousel", + component: VaultCarouselComponent, + decorators: [ + moduleMetadata({ + imports: [VaultCarouselSlideComponent, TypographyModule, ButtonComponent], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args: any) => ({ + props: args, + template: ` + + +
+

First Carousel Heading

+

First Carousel Content

+
+
+ +
+

Second Carousel Heading

+

Second Carousel Content

+
+
+ +
+

Third Carousel Heading

+

Third Carousel Content

+

Third Carousel Content

+

Third Carousel Content

+
+
+ +
+

Fourth Carousel Heading

+

Fourth Carousel Content

+
+
+
+ `, + }), +}; + +export const KeyboardNavigation: Story = { + render: (args: any) => ({ + props: args, + template: ` + + +
+

First Carousel Heading

+ +
+
+ +
+

Second Carousel Heading

+

With no focusable elements, the entire slide should be focusable

+
+
+
+ `, + }), +}; diff --git a/libs/vault/src/components/carousel/index.ts b/libs/vault/src/components/carousel/index.ts new file mode 100644 index 000000000000..a785c2610205 --- /dev/null +++ b/libs/vault/src/components/carousel/index.ts @@ -0,0 +1 @@ +export { VaultCarouselComponent } from "./carousel.component"; diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts index dc6ca16ddd63..f621ca631015 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts @@ -4,8 +4,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index a47cdc8d1d51..1445ca557cf6 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -7,8 +7,8 @@ import { NEVER, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts index fb90df4d6ace..b9f83ea2176d 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts +++ b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts @@ -9,10 +9,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl 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 { ColorPasswordModule, ItemModule } from "@bitwarden/components"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; +import { ColorPasswordComponent, ColorPasswordModule, ItemModule } from "@bitwarden/components"; import { PasswordHistoryViewComponent } from "./password-history-view.component"; diff --git a/libs/vault/src/components/password-reprompt.component.ts b/libs/vault/src/components/password-reprompt.component.ts index abefac587474..dc75d50c93eb 100644 --- a/libs/vault/src/components/password-reprompt.component.ts +++ b/libs/vault/src/components/password-reprompt.component.ts @@ -1,10 +1,11 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Component } from "@angular/core"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { @@ -13,6 +14,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + ToastService, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -45,6 +47,7 @@ export class PasswordRepromptComponent { protected i18nService: I18nService, protected formBuilder: FormBuilder, protected dialogRef: DialogRef, + private toastService: ToastService, protected accountService: AccountService, ) {} @@ -55,7 +58,7 @@ export class PasswordRepromptComponent { return; } - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (userId == null) { throw new Error("An active user is expected while doing password reprompt."); @@ -72,11 +75,11 @@ export class PasswordRepromptComponent { userId, )) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidMasterPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); return; } diff --git a/libs/vault/src/components/totp-countdown/totp-countdown.component.html b/libs/vault/src/components/totp-countdown/totp-countdown.component.html index 7de19ac3eddd..5c535a9e2704 100644 --- a/libs/vault/src/components/totp-countdown/totp-countdown.component.html +++ b/libs/vault/src/components/totp-countdown/totp-countdown.component.html @@ -6,7 +6,7 @@ bitTypography="helper" >{{ totpSec }} - + ; + + /** + * Observable of all tasks for a given user. + * @param userId + */ + abstract tasks$(userId: UserId): Observable; + + /** + * Observable of pending tasks for a given user. + * @param userId + */ + abstract pendingTasks$(userId: UserId): Observable; + + /** + * Retrieves tasks from the API for a given user and updates the local state. + * @param userId + */ + abstract refreshTasks(userId: UserId): Promise; + + /** + * Clears all the tasks from state for the given user. + * @param userId + */ + abstract clear(userId: UserId): Promise; + + /** + * Marks a task as complete in local state and updates the server. + * @param taskId - The ID of the task to mark as complete. + * @param userId - The user who is completing the task. + */ + abstract markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise; +} diff --git a/libs/vault/src/tasks/enums/index.ts b/libs/vault/src/tasks/enums/index.ts new file mode 100644 index 000000000000..c1a5e81d877f --- /dev/null +++ b/libs/vault/src/tasks/enums/index.ts @@ -0,0 +1,2 @@ +export * from "./security-task-status.enum"; +export * from "./security-task-type.enum"; diff --git a/libs/vault/src/tasks/enums/security-task-status.enum.ts b/libs/vault/src/tasks/enums/security-task-status.enum.ts new file mode 100644 index 000000000000..1c6e7decc20a --- /dev/null +++ b/libs/vault/src/tasks/enums/security-task-status.enum.ts @@ -0,0 +1,11 @@ +export enum SecurityTaskStatus { + /** + * Default status for newly created tasks that have not been completed. + */ + Pending = 0, + + /** + * Status when a task is considered complete and has no remaining actions + */ + Completed = 1, +} diff --git a/libs/vault/src/tasks/enums/security-task-type.enum.ts b/libs/vault/src/tasks/enums/security-task-type.enum.ts new file mode 100644 index 000000000000..264cd88696b8 --- /dev/null +++ b/libs/vault/src/tasks/enums/security-task-type.enum.ts @@ -0,0 +1,6 @@ +export enum SecurityTaskType { + /** + * Task to update a cipher's password that was found to be at-risk by an administrator + */ + UpdateAtRiskCredential = 0, +} diff --git a/libs/vault/src/tasks/index.ts b/libs/vault/src/tasks/index.ts new file mode 100644 index 000000000000..a0339c6d8f4c --- /dev/null +++ b/libs/vault/src/tasks/index.ts @@ -0,0 +1,5 @@ +export * from "./enums"; +export * from "./models"; + +export * from "./abstractions/task.service"; +export * from "./services/default-task.service"; diff --git a/libs/vault/src/tasks/models/index.ts b/libs/vault/src/tasks/models/index.ts new file mode 100644 index 000000000000..e9b6e3352afb --- /dev/null +++ b/libs/vault/src/tasks/models/index.ts @@ -0,0 +1,3 @@ +export * from "./security-task"; +export * from "./security-task.data"; +export * from "./security-task.response"; diff --git a/libs/vault/src/tasks/models/security-task.data.ts b/libs/vault/src/tasks/models/security-task.data.ts new file mode 100644 index 000000000000..e2d9cc76c0a1 --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.data.ts @@ -0,0 +1,34 @@ +import { Jsonify } from "type-fest"; + +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +import { SecurityTaskResponse } from "./security-task.response"; + +export class SecurityTaskData { + id: SecurityTaskId; + organizationId: OrganizationId; + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(response: SecurityTaskResponse) { + this.id = response.id; + this.organizationId = response.organizationId; + this.cipherId = response.cipherId; + this.type = response.type; + this.status = response.status; + this.creationDate = response.creationDate; + this.revisionDate = response.revisionDate; + } + + static fromJSON(obj: Jsonify) { + return Object.assign(new SecurityTaskData({} as SecurityTaskResponse), obj, { + creationDate: new Date(obj.creationDate), + revisionDate: new Date(obj.revisionDate), + }); + } +} diff --git a/libs/vault/src/tasks/models/security-task.response.ts b/libs/vault/src/tasks/models/security-task.response.ts new file mode 100644 index 000000000000..2a335eb5d2f2 --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.response.ts @@ -0,0 +1,28 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +export class SecurityTaskResponse extends BaseResponse { + id: SecurityTaskId; + organizationId: OrganizationId; + /** + * Optional cipherId for tasks that are related to a cipher. + */ + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.organizationId = this.getResponseProperty("OrganizationId"); + this.cipherId = this.getResponseProperty("CipherId") || undefined; + this.type = this.getResponseProperty("Type"); + this.status = this.getResponseProperty("Status"); + this.creationDate = this.getResponseProperty("CreationDate"); + this.revisionDate = this.getResponseProperty("RevisionDate"); + } +} diff --git a/libs/vault/src/tasks/models/security-task.ts b/libs/vault/src/tasks/models/security-task.ts new file mode 100644 index 000000000000..42adb951945f --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.ts @@ -0,0 +1,28 @@ +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +import { SecurityTaskData } from "./security-task.data"; + +export class SecurityTask { + id: SecurityTaskId; + organizationId: OrganizationId; + /** + * Optional cipherId for tasks that are related to a cipher. + */ + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(obj: SecurityTaskData) { + this.id = obj.id; + this.organizationId = obj.organizationId; + this.cipherId = obj.cipherId; + this.type = obj.type; + this.status = obj.status; + this.creationDate = obj.creationDate; + this.revisionDate = obj.revisionDate; + } +} diff --git a/libs/vault/src/tasks/services/default-task.service.spec.ts b/libs/vault/src/tasks/services/default-task.service.spec.ts new file mode 100644 index 000000000000..26b1a79ca2e9 --- /dev/null +++ b/libs/vault/src/tasks/services/default-task.service.spec.ts @@ -0,0 +1,261 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { DefaultTaskService, SecurityTaskStatus } from "@bitwarden/vault"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../common/spec"; +import { SecurityTaskData } from "../models/security-task.data"; +import { SecurityTaskResponse } from "../models/security-task.response"; +import { SECURITY_TASKS } from "../state/security-task.state"; + +describe("Default task service", () => { + let fakeStateProvider: FakeStateProvider; + + const mockApiSend = jest.fn(); + const mockGetAllOrgs$ = jest.fn(); + + let testBed: TestBed; + + beforeEach(async () => { + mockApiSend.mockClear(); + mockGetAllOrgs$.mockClear(); + + fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); + testBed = TestBed.configureTestingModule({ + imports: [], + providers: [ + DefaultTaskService, + { + provide: StateProvider, + useValue: fakeStateProvider, + }, + { + provide: ApiService, + useValue: { + send: mockApiSend, + }, + }, + { + provide: OrganizationService, + useValue: { + organizations$: mockGetAllOrgs$, + }, + }, + ], + }); + }); + + describe("tasksEnabled$", () => { + it("should emit true if any organization uses risk insights", async () => { + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: false, + }, + { + useRiskInsights: true, + }, + ] as Organization[]), + ); + + const { tasksEnabled$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasksEnabled$("user-id" as UserId)); + + expect(result).toBe(true); + }); + + it("should emit false if no organization uses risk insights", async () => { + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: false, + }, + { + useRiskInsights: false, + }, + ] as Organization[]), + ); + + const { tasksEnabled$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasksEnabled$("user-id" as UserId)); + + expect(result).toBe(false); + }); + }); + + describe("tasks$", () => { + it("should fetch tasks from the API when the state is null", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null); + + const { tasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + }); + + it("should use the tasks from state when not null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const { tasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).not.toHaveBeenCalled(); + }); + + it("should share the same observable for the same user", async () => { + const { tasks$ } = testBed.inject(DefaultTaskService); + + const first = tasks$("user-id" as UserId); + const second = tasks$("user-id" as UserId); + + expect(first).toBe(second); + }); + }); + + describe("pendingTasks$", () => { + it("should filter tasks to only pending tasks", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "completed-task-id" as SecurityTaskId, + status: SecurityTaskStatus.Completed, + }, + { + id: "pending-task-id" as SecurityTaskId, + status: SecurityTaskStatus.Pending, + }, + ] as SecurityTaskData[]); + + const { pendingTasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(pendingTasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(result[0].id).toBe("pending-task-id" as SecurityTaskId); + }); + }); + + describe("refreshTasks()", () => { + it("should fetch tasks from the API", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + const service = testBed.inject(DefaultTaskService); + + await service.refreshTasks("user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + }); + + it("should update the local state with refreshed tasks", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null); + + const service = testBed.inject(DefaultTaskService); + + await service.refreshTasks("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + }); + }); + + describe("clear()", () => { + it("should clear the local state for the user", async () => { + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const service = testBed.inject(DefaultTaskService); + + await service.clear("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([]); + }); + }); + + describe("markAsComplete()", () => { + it("should send an API request to mark the task as complete", async () => { + const service = testBed.inject(DefaultTaskService); + + await service.markAsComplete("task-id" as SecurityTaskId, "user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith( + "PATCH", + "/tasks/task-id/complete", + null, + true, + false, + ); + }); + + it("should refresh all tasks for the user after marking the task as complete", async () => { + mockApiSend + .mockResolvedValueOnce(null) // Mark as complete + .mockResolvedValueOnce({ + // Refresh tasks + data: [ + { + id: "new-task-id", + }, + ] as SecurityTaskResponse[], + }); + + const mockState = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "old-task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const service = testBed.inject(DefaultTaskService); + + await service.markAsComplete("task-id" as SecurityTaskId, "user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + expect(mockState.nextMock).toHaveBeenCalledWith([ + { + id: "new-task-id", + } as SecurityTaskData, + ]); + }); + }); +}); diff --git a/libs/vault/src/tasks/services/default-task.service.ts b/libs/vault/src/tasks/services/default-task.service.ts new file mode 100644 index 000000000000..998b20772833 --- /dev/null +++ b/libs/vault/src/tasks/services/default-task.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from "@angular/core"; +import { map, switchMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/vault"; + +import { filterOutNullish, perUserCache$ } from "../../utils/observable-utilities"; +import { SecurityTaskData } from "../models/security-task.data"; +import { SecurityTaskResponse } from "../models/security-task.response"; +import { SECURITY_TASKS } from "../state/security-task.state"; + +@Injectable() +export class DefaultTaskService implements TaskService { + constructor( + private stateProvider: StateProvider, + private apiService: ApiService, + private organizationService: OrganizationService, + ) {} + + tasksEnabled$ = perUserCache$((userId) => { + return this.organizationService + .organizations$(userId) + .pipe(map((orgs) => orgs.some((o) => o.useRiskInsights))); + }); + + tasks$ = perUserCache$((userId) => { + return this.taskState(userId).state$.pipe( + switchMap(async (tasks) => { + if (tasks == null) { + await this.fetchTasksFromApi(userId); + } + return tasks; + }), + filterOutNullish(), + map((tasks) => tasks.map((t) => new SecurityTask(t))), + ); + }); + + pendingTasks$ = perUserCache$((userId) => { + return this.tasks$(userId).pipe( + map((tasks) => tasks.filter((t) => t.status === SecurityTaskStatus.Pending)), + ); + }); + + async refreshTasks(userId: UserId): Promise { + await this.fetchTasksFromApi(userId); + } + + async clear(userId: UserId): Promise { + await this.updateTaskState(userId, []); + } + + async markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise { + await this.apiService.send("PATCH", `/tasks/${taskId}/complete`, null, true, false); + await this.refreshTasks(userId); + } + + /** + * Fetches the tasks from the API and updates the local state + * @param userId + * @private + */ + private async fetchTasksFromApi(userId: UserId): Promise { + const r = await this.apiService.send("GET", "/tasks", null, true, true); + const response = new ListResponse(r, SecurityTaskResponse); + + const taskData = response.data.map((t) => new SecurityTaskData(t)); + await this.updateTaskState(userId, taskData); + } + + /** + * Returns the local state for the tasks + * @param userId + * @private + */ + private taskState(userId: UserId) { + return this.stateProvider.getUser(userId, SECURITY_TASKS); + } + + /** + * Updates the local state with the provided tasks and returns the updated state + * @param userId + * @param tasks + * @private + */ + private updateTaskState( + userId: UserId, + tasks: SecurityTaskData[], + ): Promise { + return this.taskState(userId).update(() => tasks); + } +} diff --git a/libs/vault/src/tasks/state/security-task.state.ts b/libs/vault/src/tasks/state/security-task.state.ts new file mode 100644 index 000000000000..b86a891f0088 --- /dev/null +++ b/libs/vault/src/tasks/state/security-task.state.ts @@ -0,0 +1,14 @@ +import { Jsonify } from "type-fest"; + +import { SECURITY_TASKS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; + +import { SecurityTaskData } from "../models/security-task.data"; + +export const SECURITY_TASKS = UserKeyDefinition.array( + SECURITY_TASKS_DISK, + "securityTasks", + { + deserializer: (task: Jsonify) => SecurityTaskData.fromJSON(task), + clearOn: ["logout", "lock"], + }, +); diff --git a/libs/vault/src/utils/observable-utilities.ts b/libs/vault/src/utils/observable-utilities.ts new file mode 100644 index 000000000000..bb559c600d34 --- /dev/null +++ b/libs/vault/src/utils/observable-utilities.ts @@ -0,0 +1,37 @@ +import { filter, Observable, OperatorFunction, shareReplay } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; + +/** + * Builds an observable once per userId and caches it for future requests. + * The built observables are shared among subscribers with a replay buffer size of 1. + * @param create - A function that creates an observable for a given userId. + */ +export function perUserCache$( + create: (userId: UserId) => Observable, +): (userId: UserId) => Observable { + const cache = new Map>(); + return (userId: UserId) => { + let observable = cache.get(userId); + if (!observable) { + observable = create(userId).pipe(shareReplay({ bufferSize: 1, refCount: false })); + cache.set(userId, observable); + } + return observable; + }; +} + +/** + * Strongly typed observable operator that filters out null/undefined values and adjusts the return type to + * be non-nullable. + * + * @example + * ```ts + * const source$ = of(1, null, 2, undefined, 3); + * source$.pipe(filterOutNullish()).subscribe(console.log); + * // Output: 1, 2, 3 + * ``` + */ +export function filterOutNullish(): OperatorFunction { + return filter((v): v is T => v != null); +} diff --git a/libs/vault/tsconfig.json b/libs/vault/tsconfig.json index 8318212e81d4..e1515183f22d 100644 --- a/libs/vault/tsconfig.json +++ b/libs/vault/tsconfig.json @@ -15,6 +15,7 @@ "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], "@bitwarden/key-management": ["../key-management/src"], "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], "@bitwarden/vault": ["../vault/src"] } }, diff --git a/package-lock.json b/package-lock.json index 12a1468e3b75..3883aec16871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,12 +36,12 @@ "argon2-browser": "1.18.0", "big-integer": "1.6.52", "bootstrap": "4.6.0", - "braintree-web-drop-in": "1.43.0", + "braintree-web-drop-in": "1.44.0", "buffer": "6.0.3", "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.39.0", + "core-js": "3.40.0", "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", @@ -55,7 +55,6 @@ "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "1.4.5-lts.1", - "ngx-infinite-scroll": "18.0.0", "ngx-toastr": "19.0.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", @@ -70,17 +69,14 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.71", + "tldts": "6.1.74", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" }, "devDependencies": { "@angular-devkit/build-angular": "18.2.12", - "@angular-eslint/eslint-plugin": "18.4.3", - "@angular-eslint/eslint-plugin-template": "18.4.3", "@angular-eslint/schematics": "18.4.3", - "@angular-eslint/template-parser": "18.4.3", "@angular/cli": "18.2.12", "@angular/compiler-cli": "18.2.13", "@babel/core": "7.24.9", @@ -89,16 +85,16 @@ "@electron/notarize": "2.5.0", "@electron/rebuild": "3.7.1", "@ngtools/webpack": "18.2.12", - "@storybook/addon-a11y": "8.4.7", - "@storybook/addon-actions": "8.4.7", + "@storybook/addon-a11y": "8.5.2", + "@storybook/addon-actions": "8.5.2", "@storybook/addon-designs": "8.0.4", - "@storybook/addon-essentials": "8.4.7", - "@storybook/addon-interactions": "8.4.7", - "@storybook/addon-links": "8.4.7", - "@storybook/angular": "8.4.7", - "@storybook/manager-api": "8.4.7", - "@storybook/theming": "8.4.7", - "@storybook/web-components-webpack5": "8.4.7", + "@storybook/addon-essentials": "8.5.2", + "@storybook/addon-interactions": "8.5.2", + "@storybook/addon-links": "8.5.2", + "@storybook/angular": "8.5.2", + "@storybook/manager-api": "8.5.2", + "@storybook/theming": "8.5.2", + "@storybook/web-components-webpack5": "8.5.2", "@types/argon2-browser": "1.18.4", "@types/chrome": "0.0.280", "@types/firefox-webext-browser": "120.0.4", @@ -113,7 +109,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.5", + "@types/node": "22.10.7", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -121,33 +117,34 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/eslint-plugin": "8.20.0", - "@typescript-eslint/parser": "8.20.0", + "@typescript-eslint/rule-tester": "8.22.0", + "@typescript-eslint/utils": "8.22.0", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", + "angular-eslint": "18.4.3", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", - "chromatic": "11.20.2", + "chromatic": "11.25.2", "concurrently": "9.1.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.3.1", + "electron": "34.0.0", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", "electron-updater": "6.3.9", "eslint": "8.57.1", - "eslint-config-prettier": "9.1.0", + "eslint-config-prettier": "10.0.1", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-storybook": "0.11.2", - "eslint-plugin-tailwindcss": "3.17.5", + "eslint-plugin-tailwindcss": "3.18.0", "html-loader": "5.1.0", "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.3", @@ -156,30 +153,31 @@ "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", - "lint-staged": "15.4.0", + "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-loader": "8.1.1", "prettier": "3.4.2", - "prettier-plugin-tailwindcss": "0.6.9", + "prettier-plugin-tailwindcss": "0.6.10", "process": "0.11.10", "remark-gfm": "4.0.0", "rimraf": "6.0.1", - "sass": "1.83.1", + "sass": "1.83.4", "sass-loader": "16.0.4", - "storybook": "8.4.7", + "storybook": "8.5.2", "style-loader": "4.0.0", "tailwindcss": "3.4.17", "ts-jest": "29.2.2", - "ts-loader": "9.5.1", + "ts-loader": "9.5.2", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", "typescript": "5.4.2", - "typescript-strict-plugin": "^2.4.4", + "typescript-eslint": "8.20.0", + "typescript-strict-plugin": "2.4.4", "url": "0.11.4", "util": "0.12.5", - "wait-on": "8.0.1", + "wait-on": "8.0.2", "webpack": "5.97.1", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.0", @@ -192,11 +190,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.1.1" + "version": "2025.2.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.1.1", + "version": "2025.2.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -223,7 +221,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.71", + "tldts": "6.1.74", "zxcvbn": "4.4.2" }, "bin": { @@ -232,7 +230,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.1.3", + "version": "2025.2.1", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -246,7 +244,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.1.1" + "version": "2025.2.0" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -296,12 +294,12 @@ "libs/key-management": { "name": "@bitwarden/key-management", "version": "0.0.0", - "license": "GPL-3.0", - "dependencies": { - "@bitwarden/angular": "file:../angular", - "@bitwarden/common": "file:../common", - "@bitwarden/components": "file:../components" - } + "license": "GPL-3.0" + }, + "libs/key-management-ui": { + "name": "@bitwarden/key-management-ui", + "version": "0.0.0", + "license": "GPL-3.0" }, "libs/node": { "name": "@bitwarden/node", @@ -1240,6 +1238,20 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-eslint/builder": { + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.4.3.tgz", + "integrity": "sha512-NzmrXlr7GFE+cjwipY/CxBscZXNqnuK0us1mO6Z2T6MeH6m+rRcdlY/rZyKoRniyNNvuzl6vpEsfMIMmnfebrA==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": ">= 0.1800.0 < 0.1900.0", + "@angular-devkit/core": ">= 18.0.0 < 19.0.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, "node_modules/@angular-eslint/bundled-angular-compiler": { "version": "18.4.3", "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.4.3.tgz", @@ -4422,6 +4434,10 @@ "resolved": "libs/key-management", "link": true }, + "node_modules/@bitwarden/key-management-ui": { + "resolved": "libs/key-management-ui", + "link": true + }, "node_modules/@bitwarden/node": { "resolved": "libs/node", "link": true @@ -7905,6 +7921,36 @@ "license": "MIT", "optional": true }, + "node_modules/@paypal/accelerated-checkout-loader": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@paypal/accelerated-checkout-loader/-/accelerated-checkout-loader-1.1.0.tgz", + "integrity": "sha512-S2KkIpq15VnxYyI0tycvfYiNsqdsg2a92El2huYUVLsWnBbubl8toYK8khaP5nnxZ0MGl9mEB9Y9axmfOw2Yvg==", + "license": "MIT", + "dependencies": { + "@braintree/asset-loader": "2.0.0", + "envify": "^4.1.0", + "typescript": "^4.6.4" + } + }, + "node_modules/@paypal/accelerated-checkout-loader/node_modules/@braintree/asset-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@braintree/asset-loader/-/asset-loader-2.0.0.tgz", + "integrity": "sha512-7Zs3/g3lPTfkdtWr7cKh3tk1pDruXR++TXwGKkx7BPuTjjLNFul2JSfI+ScHzNU4u/gZNPNQagsSTlYxIhBgMA==", + "license": "MIT" + }, + "node_modules/@paypal/accelerated-checkout-loader/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@phc/format": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", @@ -8274,27 +8320,29 @@ } }, "node_modules/@storybook/addon-a11y": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.4.7.tgz", - "integrity": "sha512-GpUvXp6n25U1ZSv+hmDC+05BEqxWdlWjQTb/GaboRXZQeMBlze6zckpVb66spjmmtQAIISo0eZxX1+mGcVR7lA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.5.2.tgz", + "integrity": "sha512-GhZrDfqhZ9l6egFcyAgjO6g0iaTJCDO/H0NOAadLrw55aO1apo07H12YoWtJeA00wUqvuufmh5DGo/CExLvgSQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/addon-highlight": "8.4.7", - "axe-core": "^4.2.0" + "@storybook/addon-highlight": "8.5.2", + "@storybook/test": "8.5.2", + "axe-core": "^4.2.0", + "vitest-axe": "^0.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-actions": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.4.7.tgz", - "integrity": "sha512-mjtD5JxcPuW74T6h7nqMxWTvDneFtokg88p6kQ5OnC1M259iAXb//yiSZgu/quunMHPCXSiqn4FNOSgASTSbsA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.5.2.tgz", + "integrity": "sha512-g0gLesVSFgstUq5QphsLeC1vEdwNHgqo2TE0m+STM47832xbxBwmK6uvBeqi416xZvnt1TTKaaBr4uCRRQ64Ww==", "dev": true, "license": "MIT", "dependencies": { @@ -8309,13 +8357,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-backgrounds": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.4.7.tgz", - "integrity": "sha512-I4/aErqtFiazcoWyKafOAm3bLpxTj6eQuH/woSbk1Yx+EzN+Dbrgx1Updy8//bsNtKkcrXETITreqHC+a57DHQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.5.2.tgz", + "integrity": "sha512-l9WkI4QHfINeFQkW9K0joaM7WweKktwIIyUPEvyoupHT4n9ccJHAlWjH4SBmzwI1j1Zt0G3t+bq8mVk/YK6Fsg==", "dev": true, "license": "MIT", "dependencies": { @@ -8328,13 +8376,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-controls": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.4.7.tgz", - "integrity": "sha512-377uo5IsJgXLnQLJixa47+11V+7Wn9KcDEw+96aGCBCfLbWNH8S08tJHHnSu+jXg9zoqCAC23MetntVp6LetHA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.5.2.tgz", + "integrity": "sha512-wkzw2vRff4zkzdvC/GOlB2PlV0i973u8igSLeg34TWNEAa4bipwVHnFfIojRuP9eN1bZL/0tjuU5pKnbTqH7aQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8347,7 +8395,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-designs": { @@ -8385,16 +8433,16 @@ } }, "node_modules/@storybook/addon-docs": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.4.7.tgz", - "integrity": "sha512-NwWaiTDT5puCBSUOVuf6ME7Zsbwz7Y79WF5tMZBx/sLQ60vpmJVQsap6NSjvK1Ravhc21EsIXqemAcBjAWu80w==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.5.2.tgz", + "integrity": "sha512-pRLJ/Qb/3XHpjS7ZAMaOZYtqxOuI8wPxVKYQ6n5rfMSj2jFwt5tdDsEJdhj2t5lsY8HrzEZi8ExuW5I5RoUoIQ==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/blocks": "8.4.7", - "@storybook/csf-plugin": "8.4.7", - "@storybook/react-dom-shim": "8.4.7", + "@storybook/blocks": "8.5.2", + "@storybook/csf-plugin": "8.5.2", + "@storybook/react-dom-shim": "8.5.2", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "ts-dedent": "^2.0.0" @@ -8404,25 +8452,25 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-essentials": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.4.7.tgz", - "integrity": "sha512-+BtZHCBrYtQKILtejKxh0CDRGIgTl9PumfBOKRaihYb4FX1IjSAxoV/oo/IfEjlkF5f87vouShWsRa8EUauFDw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.5.2.tgz", + "integrity": "sha512-MfojJKxDg0bnjOE0MfLSaPweAud1Esjaf1D9M8EYnpeFnKGZApcGJNRpHCDiHrS5BMr8hHa58RDVc7ObFTI4Dw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/addon-actions": "8.4.7", - "@storybook/addon-backgrounds": "8.4.7", - "@storybook/addon-controls": "8.4.7", - "@storybook/addon-docs": "8.4.7", - "@storybook/addon-highlight": "8.4.7", - "@storybook/addon-measure": "8.4.7", - "@storybook/addon-outline": "8.4.7", - "@storybook/addon-toolbars": "8.4.7", - "@storybook/addon-viewport": "8.4.7", + "@storybook/addon-actions": "8.5.2", + "@storybook/addon-backgrounds": "8.5.2", + "@storybook/addon-controls": "8.5.2", + "@storybook/addon-docs": "8.5.2", + "@storybook/addon-highlight": "8.5.2", + "@storybook/addon-measure": "8.5.2", + "@storybook/addon-outline": "8.5.2", + "@storybook/addon-toolbars": "8.5.2", + "@storybook/addon-viewport": "8.5.2", "ts-dedent": "^2.0.0" }, "funding": { @@ -8430,13 +8478,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-highlight": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.4.7.tgz", - "integrity": "sha512-whQIDBd3PfVwcUCrRXvCUHWClXe9mQ7XkTPCdPo4B/tZ6Z9c6zD8JUHT76ddyHivixFLowMnA8PxMU6kCMAiNw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.5.2.tgz", + "integrity": "sha512-QjJfY+8e1bi6FeGfVlgxzv/I8DUyC83lZq8zfTY7nDUCVdmKi8VzmW0KgDo5PaEOFKs8x6LKJa+s5O0gFQaJMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8447,19 +8495,19 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-interactions": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.4.7.tgz", - "integrity": "sha512-fnufT3ym8ht3HHUIRVXAH47iOJW/QOb0VSM+j269gDuvyDcY03D1civCu1v+eZLGaXPKJ8vtjr0L8zKQ/4P0JQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.5.2.tgz", + "integrity": "sha512-Gn9Egk2OS0BkkHd671Y0pIqBr4noAOLUfnpxhHE8r0Tt7FmJFeVSN+dqK7hQeUmKL5jdSY25FTYROg65JmtGOA==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/instrumenter": "8.4.7", - "@storybook/test": "8.4.7", + "@storybook/instrumenter": "8.5.2", + "@storybook/test": "8.5.2", "polished": "^4.2.2", "ts-dedent": "^2.2.0" }, @@ -8468,17 +8516,17 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-links": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.4.7.tgz", - "integrity": "sha512-L/1h4dMeMKF+MM0DanN24v5p3faNYbbtOApMgg7SlcBT/tgo3+cAjkgmNpYA8XtKnDezm+T2mTDhB8mmIRZpIQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.5.2.tgz", + "integrity": "sha512-eDKOQoAKKUQo0JqeLNzMLu6fm1s3oxwZ6O+rAWS6n5bsrjZS2Ul8esKkRriFVwHtDtqx99wneqOscS8IzE/ENw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf": "^0.1.11", + "@storybook/csf": "0.1.12", "@storybook/global": "^5.0.0", "ts-dedent": "^2.0.0" }, @@ -8488,7 +8536,7 @@ }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.7" + "storybook": "^8.5.2" }, "peerDependenciesMeta": { "react": { @@ -8497,9 +8545,9 @@ } }, "node_modules/@storybook/addon-measure": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.4.7.tgz", - "integrity": "sha512-QfvqYWDSI5F68mKvafEmZic3SMiK7zZM8VA0kTXx55hF/+vx61Mm0HccApUT96xCXIgmwQwDvn9gS4TkX81Dmw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.5.2.tgz", + "integrity": "sha512-g7Kvrx8dqzeYWetpWYVVu4HaRzLAZVlOAlZYNfCH/aJHcFKp/p5zhPXnZh8aorxeCLHW1QSKcliaA4BNPEvTeg==", "dev": true, "license": "MIT", "dependencies": { @@ -8511,13 +8559,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-outline": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.4.7.tgz", - "integrity": "sha512-6LYRqUZxSodmAIl8icr585Oi8pmzbZ90aloZJIpve+dBAzo7ydYrSQxxoQEVltXbKf3VeVcrs64ouAYqjisMYA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.5.2.tgz", + "integrity": "sha512-laMVLT1xluSqMa2mMzmS1kdKcjX0HI9Fw+7pM3r4drtGWtxpyBT32YFqKfWFIBhcd364ti2tDUz9FlygGQ1rKw==", "dev": true, "license": "MIT", "dependencies": { @@ -8529,13 +8577,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-toolbars": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.4.7.tgz", - "integrity": "sha512-OSfdv5UZs+NdGB+nZmbafGUWimiweJ/56gShlw8Neo/4jOJl1R3rnRqqY7MYx8E4GwoX+i3GF5C3iWFNQqlDcw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.5.2.tgz", + "integrity": "sha512-gHQtVCiq7HRqdYQLOmX8nhtV1Lqz4tOCj4BVodwwf8fUcHyNor+2FvGlQjngV2pIeCtxiM/qmG63UpTBp57ZMA==", "dev": true, "license": "MIT", "funding": { @@ -8543,13 +8591,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/addon-viewport": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.4.7.tgz", - "integrity": "sha512-hvczh/jjuXXcOogih09a663sRDDSATXwbE866al1DXgbDFraYD/LxX/QDb38W9hdjU9+Qhx8VFIcNWoMQns5HQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.5.2.tgz", + "integrity": "sha512-W+7nrMQmxHcUNGsXjmb/fak1mD0a5vf4y1hBhSM7/131t8KBsvEu4ral8LTUhc4ZzuU1eIUM0Qth7SjqHqm5bA==", "dev": true, "license": "MIT", "dependencies": { @@ -8560,24 +8608,23 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/angular": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-8.4.7.tgz", - "integrity": "sha512-PYWWEvoe+sT8riprSQVCyGnQbifbuzT9YNYPi22YBxB8ZGVuIVwjshKjSZvC99ULQbMvJ/g2OPCcBA8hhc3aTg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-8.5.2.tgz", + "integrity": "sha512-vYfbzckQvvFqwc5/5oDOOP2Gx7Dcq5KQpwPFHPSNH9TLBIXHk4Hjklgn62k6sxk1YIWRJJNo8GWERpgN0JOXxQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-webpack5": "8.4.7", - "@storybook/components": "8.4.7", - "@storybook/core-webpack": "8.4.7", + "@storybook/builder-webpack5": "8.5.2", + "@storybook/components": "8.5.2", + "@storybook/core-webpack": "8.5.2", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "8.4.7", - "@storybook/preview-api": "8.4.7", - "@storybook/theming": "8.4.7", - "@types/node": "^22.0.0", + "@storybook/manager-api": "8.5.2", + "@storybook/preview-api": "8.5.2", + "@storybook/theming": "8.5.2", "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", "@types/semver": "^7.3.4", @@ -8611,7 +8658,7 @@ "@angular/platform-browser": ">=15.0.0 < 20.0.0", "@angular/platform-browser-dynamic": ">=15.0.0 < 20.0.0", "rxjs": "^6.0.0 || ^7.4.0", - "storybook": "^8.4.7", + "storybook": "^8.5.2", "typescript": "^4.0.0 || ^5.0.0", "zone.js": ">= 0.11.1 < 1.0.0" }, @@ -8622,13 +8669,13 @@ } }, "node_modules/@storybook/blocks": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.4.7.tgz", - "integrity": "sha512-+QH7+JwXXXIyP3fRCxz/7E2VZepAanXJM7G8nbR3wWsqWgrRp4Wra6MvybxAYCxU7aNfJX5c+RW84SNikFpcIA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.5.2.tgz", + "integrity": "sha512-C6Bz/YTG5ZuyAzglqgqozYUWaS39j1PnkVuMNots6S3Fp8ZJ6iZOlQ+rpumiuvnbfD5rkEZG+614RWNyNlFy7g==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf": "^0.1.11", + "@storybook/csf": "0.1.12", "@storybook/icons": "^1.2.12", "ts-dedent": "^2.0.0" }, @@ -8639,7 +8686,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.7" + "storybook": "^8.5.2" }, "peerDependenciesMeta": { "react": { @@ -8651,14 +8698,13 @@ } }, "node_modules/@storybook/builder-webpack5": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.4.7.tgz", - "integrity": "sha512-O8LpsQ+4g2x5kh7rI9+jEUdX8k1a5egBQU1lbudmHchqsV0IKiVqBD9LL5Gj3wpit4vB8coSW4ZWTFBw8FQb4Q==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.5.2.tgz", + "integrity": "sha512-P4zpavhy9cL1GtITlFp1amTgNSfaQyi60jJwi7joUj0z4RRyBr8YpGi5il9PlaxiY2HROsCdKJftTNzWn058yA==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core-webpack": "8.4.7", - "@types/node": "^22.0.0", + "@storybook/core-webpack": "8.5.2", "@types/semver": "^7.3.4", "browser-assert": "^1.2.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", @@ -8688,7 +8734,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" }, "peerDependenciesMeta": { "typescript": { @@ -8750,9 +8796,9 @@ } }, "node_modules/@storybook/components": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.7.tgz", - "integrity": "sha512-uyJIcoyeMWKAvjrG9tJBUCKxr2WZk+PomgrgrUwejkIfXMO76i6jw9BwLa0NZjYdlthDv30r9FfbYZyeNPmF0g==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.5.2.tgz", + "integrity": "sha512-o5vNN30sGLTJBeGk5SKyekR4RfTpBTGs2LDjXGAmpl2MRhzd62ix8g+KIXSR0rQ55TCvKUl5VR2i99ttlRcEKw==", "dev": true, "license": "MIT", "funding": { @@ -8764,13 +8810,13 @@ } }, "node_modules/@storybook/core": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.4.7.tgz", - "integrity": "sha512-7Z8Z0A+1YnhrrSXoKKwFFI4gnsLbWzr8fnDCU6+6HlDukFYh8GHRcZ9zKfqmy6U3hw2h8H5DrHsxWfyaYUUOoA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.5.2.tgz", + "integrity": "sha512-rCOpXZo2XbdKVnZiv8oC9FId/gLkStpKGGL7hhdg/RyjcyUyTfhsvaf7LXKZH2A0n/UpwFxhF3idRfhgc1XiSg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf": "^0.1.11", + "@storybook/csf": "0.1.12", "better-opn": "^3.0.2", "browser-assert": "^1.2.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", @@ -8796,13 +8842,12 @@ } }, "node_modules/@storybook/core-webpack": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-8.4.7.tgz", - "integrity": "sha512-Tj+CjQLpFyBJxhhMms+vbPT3+gTRAiQlrhY3L1IEVwBa3wtRMS0qjozH26d1hK4G6mUIEdwu13L54HMU/w33Sg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-8.5.2.tgz", + "integrity": "sha512-r+s3zNojxl370CCCmvj0A+N27fW6zjRODQ7jsHWGSQzTDIz5Vj68rJBIOffr/27nN9r/JtbXbwxuO2UfqMqcqA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "^22.0.0", "ts-dedent": "^2.0.0" }, "funding": { @@ -8810,13 +8855,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/csf": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.11.tgz", - "integrity": "sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.12.tgz", + "integrity": "sha512-9/exVhabisyIVL0VxTCxo01Tdm8wefIXKXfltAPTSr8cbLn5JAxGQ6QV3mjdecLGEOucfoVhAKtJfVHxEK1iqw==", "dev": true, "license": "MIT", "dependencies": { @@ -8824,9 +8869,9 @@ } }, "node_modules/@storybook/csf-plugin": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.4.7.tgz", - "integrity": "sha512-Fgogplu4HImgC+AYDcdGm1rmL6OR1rVdNX1Be9C/NEXwOCpbbBwi0BxTf/2ZxHRk9fCeaPEcOdP5S8QHfltc1g==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.5.2.tgz", + "integrity": "sha512-EEQ3Vc9qIUbLH8tunzN/GSoyP3zPpNPKegZooYQbgVqA582Pel4Jnpn4uxGaOWtFCLhXMETV05X/7chGZtEujA==", "dev": true, "license": "MIT", "dependencies": { @@ -8837,7 +8882,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/global": { @@ -8862,9 +8907,9 @@ } }, "node_modules/@storybook/instrumenter": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.4.7.tgz", - "integrity": "sha512-k6NSD3jaRCCHAFtqXZ7tw8jAzD/yTEWXGya+REgZqq5RCkmJ+9S4Ytp/6OhQMPtPFX23gAuJJzTQVLcCr+gjRg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.5.2.tgz", + "integrity": "sha512-BbaUw9GXVzRg3Km95t2mRu4W6C1n1erjzll5maBaVe2+lV9MbCvBcdYwGUgjFNlQ/ETgq6vLfLOEtziycq/B6g==", "dev": true, "license": "MIT", "dependencies": { @@ -8876,13 +8921,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/manager-api": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.4.7.tgz", - "integrity": "sha512-ELqemTviCxAsZ5tqUz39sDmQkvhVAvAgiplYy9Uf15kO0SP2+HKsCMzlrm2ue2FfkUNyqbDayCPPCB0Cdn/mpQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.5.2.tgz", + "integrity": "sha512-Cn+oINA6BOO2GmGHinGsOWnEpoBnurlZ9ekMq7H/c1SYMvQWNg5RlELyrhsnyhNd83fqFZy9Asb0RXI8oqz7DQ==", "dev": true, "license": "MIT", "funding": { @@ -8894,9 +8939,9 @@ } }, "node_modules/@storybook/preview-api": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.7.tgz", - "integrity": "sha512-0QVQwHw+OyZGHAJEXo6Knx+6/4er7n2rTDE5RYJ9F2E2Lg42E19pfdLlq2Jhoods2Xrclo3wj6GWR//Ahi39Eg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.5.2.tgz", + "integrity": "sha512-AOOaBjwnkFU40Fi68fvAnK0gMWPz6o/AmH44yDGsHgbI07UgqxLBKCTpjCGPlyQd5ezEjmGwwFTmcmq5dG8DKA==", "dev": true, "license": "MIT", "funding": { @@ -8908,9 +8953,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.4.7.tgz", - "integrity": "sha512-6bkG2jvKTmWrmVzCgwpTxwIugd7Lu+2btsLAqhQSzDyIj2/uhMNp8xIMr/NBDtLgq3nomt9gefNa9xxLwk/OMg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.5.2.tgz", + "integrity": "sha512-lt7XoaeWI8iPlWnWzIm/Wam9TpRFhlqP0KZJoKwDyHiCByqkeMrw5MJREyWq626nf34bOW8D6vkuyTzCHGTxKg==", "dev": true, "license": "MIT", "funding": { @@ -8920,19 +8965,19 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/test": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.4.7.tgz", - "integrity": "sha512-AhvJsu5zl3uG40itSQVuSy5WByp3UVhS6xAnme4FWRwgSxhvZjATJ3AZkkHWOYjnnk+P2/sbz/XuPli1FVCWoQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.5.2.tgz", + "integrity": "sha512-F5WfD75m25ZRS19cSxCzHWJ/rH8jWwIjhBlhU+UW+5xjnTS1cJuC1yPT/5Jw0/0Aj9zG1atyfBUYnNHYtsBDYQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf": "^0.1.11", + "@storybook/csf": "0.1.12", "@storybook/global": "^5.0.0", - "@storybook/instrumenter": "8.4.7", + "@storybook/instrumenter": "8.5.2", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.5.0", "@testing-library/user-event": "14.5.2", @@ -8944,13 +8989,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/theming": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.7.tgz", - "integrity": "sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.5.2.tgz", + "integrity": "sha512-vro8vJx16rIE0UehawEZbxFFA4/VGYS20PMKP6Y6Fpsce0t2/cF/U9qg3jOzVb/XDwfx+ne3/V+8rjfWx8wwJw==", "dev": true, "license": "MIT", "funding": { @@ -8962,17 +9007,17 @@ } }, "node_modules/@storybook/web-components": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.4.7.tgz", - "integrity": "sha512-zR/bUWGkS5uxvqfXnW082ScrC4y5UrTdE1VKasezLGi5bTLub2hz8JP87PJgtWrq+mdrdmkLGzv5O4iJ/tlMAw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.5.2.tgz", + "integrity": "sha512-tjogJWTuf01957eLVPSuOlS0p/06uznIJj0xRjh8j3zxELIbTpY64dLKZ3i5PR9slGIJwoK8IVuy5B1jpD+PMA==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/components": "8.4.7", + "@storybook/components": "8.5.2", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "8.4.7", - "@storybook/preview-api": "8.4.7", - "@storybook/theming": "8.4.7", + "@storybook/manager-api": "8.5.2", + "@storybook/preview-api": "8.5.2", + "@storybook/theming": "8.5.2", "tiny-invariant": "^1.3.1", "ts-dedent": "^2.0.0" }, @@ -8985,19 +9030,18 @@ }, "peerDependencies": { "lit": "^2.0.0 || ^3.0.0", - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@storybook/web-components-webpack5": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/web-components-webpack5/-/web-components-webpack5-8.4.7.tgz", - "integrity": "sha512-RgLFQB7F4FOX5nOK3byaCo5Gs8nKMq1uNswOXdHSgZKfJfaZxmyMMGmnVUmOOLECsxyREokHwRDKma8SgFrRRA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/web-components-webpack5/-/web-components-webpack5-8.5.2.tgz", + "integrity": "sha512-7msCQ1zjs21SJlA4DxwZn7nz/a4XR7Aj6QHxxL6KgIFkTtwE0iwdKHyVo6IExaSCtqqDsCiLTx9aVGYSekOoFA==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-webpack5": "8.4.7", - "@storybook/web-components": "8.4.7", - "@types/node": "^22.0.0" + "@storybook/builder-webpack5": "8.5.2", + "@storybook/web-components": "8.5.2" }, "engines": { "node": ">=18.0.0" @@ -9008,7 +9052,7 @@ }, "peerDependencies": { "lit": "^2.0.0 || ^3.0.0", - "storybook": "^8.4.7" + "storybook": "^8.5.2" } }, "node_modules/@szmarczak/http-timer": { @@ -9749,9 +9793,9 @@ } }, "node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "dev": true, "license": "MIT", "dependencies": { @@ -9840,9 +9884,9 @@ } }, "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "dev": true, "license": "MIT" }, @@ -9871,9 +9915,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", - "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", + "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", "dev": true, "license": "MIT", "dependencies": { @@ -10080,7 +10124,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.20.0", @@ -10105,6 +10148,29 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/@typescript-eslint/experimental-utils": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", @@ -10259,7 +10325,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/scope-manager": "8.20.0", "@typescript-eslint/types": "8.20.0", @@ -10279,6 +10344,120 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/@typescript-eslint/rule-tester": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.22.0.tgz", + "integrity": "sha512-krTIaDM08bSQ9sIpqDTP0aX4P0Ck/WQpj+7uMIeNqzzWEWmoJFyle12B0Na15KwBLPV2MJPmaZUn+v2qenXjaw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/utils": "8.22.0", + "ajv": "^6.12.6", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "4.6.2", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/types": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", + "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz", + "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz", + "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/rule-tester/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@typescript-eslint/rule-tester/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/rule-tester/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", @@ -10302,7 +10481,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "8.20.0", "@typescript-eslint/utils": "8.20.0", @@ -10321,6 +10499,29 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/@typescript-eslint/types": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", @@ -10363,16 +10564,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", - "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz", + "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0" + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10386,6 +10586,91 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz", + "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", + "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz", + "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz", + "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", @@ -10482,10 +10767,63 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.4.tgz", + "integrity": "sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/spy": "3.0.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/@vitest/spy": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.4.tgz", + "integrity": "sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/@vitest/pretty-format": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.6.tgz", - "integrity": "sha512-exZyLcEnHgDMKc54TtHca4McV4sKT+NKAe9ix/yhd/qkYb/TP8HTyXRFDijV19qKqTZM0hPL4753zU/U8L/gAA==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10495,6 +10833,114 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/runner": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.4.tgz", + "integrity": "sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "3.0.4", + "pathe": "^2.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", + "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", + "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/pretty-format": "3.0.4", + "loupe": "^3.1.2", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.4.tgz", + "integrity": "sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/pretty-format": "3.0.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", + "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@vitest/spy": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", @@ -10509,13 +10955,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.6.tgz", - "integrity": "sha512-ixNkFy3k4vokOUTU2blIUvOgKq/N2PW8vKIjZZYsGJCMX69MRa9J2sKqX5hY/k5O5Gty3YJChepkqZ3KM9LyIQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.6", + "@vitest/pretty-format": "2.1.8", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, @@ -11106,6 +11552,28 @@ "ajv": "^8.8.2" } }, + "node_modules/angular-eslint": { + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-18.4.3.tgz", + "integrity": "sha512-0ZjLzzADGRLUhZC8ZpwSo6CE/m6QhQB/oljMJ0mEfP+lB1sy1v8PBKNsJboIcfEEgGW669Z/efVQ3df88yJLYg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": ">= 18.0.0 < 19.0.0", + "@angular-devkit/schematics": ">= 18.0.0 < 19.0.0", + "@angular-eslint/builder": "18.4.3", + "@angular-eslint/eslint-plugin": "18.4.3", + "@angular-eslint/eslint-plugin-template": "18.4.3", + "@angular-eslint/schematics": "18.4.3", + "@angular-eslint/template-parser": "18.4.3", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*", + "typescript-eslint": "^8.0.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -11879,9 +12347,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dev": true, "license": "MIT", "dependencies": { @@ -12432,9 +12900,9 @@ } }, "node_modules/braintree-web": { - "version": "3.103.0", - "resolved": "https://registry.npmjs.org/braintree-web/-/braintree-web-3.103.0.tgz", - "integrity": "sha512-gwmC5LSUP5VUC2HmUyaFnEyLjRRAo1iKKHS5eD9KIAZHB7cAQ2il1V1q2f5zdz7+7EE11eSHXznj6n/Qm6jp6w==", + "version": "3.113.0", + "resolved": "https://registry.npmjs.org/braintree-web/-/braintree-web-3.113.0.tgz", + "integrity": "sha512-qykYxZyld4X1tRNgXZQ3ZGzmhDGTBTRQ6Q24KaG9PuYqo+P2TVDEDOVC6tRbkx2RUIdXLv2M6WpkG7oLqEia9Q==", "license": "MIT", "dependencies": { "@braintree/asset-loader": "2.0.1", @@ -12445,6 +12913,7 @@ "@braintree/sanitize-url": "7.0.4", "@braintree/uuid": "1.0.0", "@braintree/wrap-promise": "2.1.0", + "@paypal/accelerated-checkout-loader": "1.1.0", "card-validator": "10.0.0", "credit-card-type": "10.0.1", "framebus": "6.0.0", @@ -12454,9 +12923,9 @@ } }, "node_modules/braintree-web-drop-in": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/braintree-web-drop-in/-/braintree-web-drop-in-1.43.0.tgz", - "integrity": "sha512-lkUpQfYXR0CGtR7mPRR17AnZoYkHjhycxVnMGIPcWT6JPagEZcG/7tYyy34iWjYZeGa2wsquLBDV2Xeita962Q==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/braintree-web-drop-in/-/braintree-web-drop-in-1.44.0.tgz", + "integrity": "sha512-maOq9SwiXztIzixJhOras7K44x4UIqqnkyQMYAJqxQ8WkADv9AkflCu2j3IeVYCus/Th9gWWFHcBugn3C4sZGw==", "license": "MIT", "dependencies": { "@braintree/asset-loader": "2.0.1", @@ -12464,7 +12933,7 @@ "@braintree/event-emitter": "0.4.1", "@braintree/uuid": "1.0.0", "@braintree/wrap-promise": "2.1.0", - "braintree-web": "3.103.0" + "braintree-web": "3.113.0" } }, "node_modules/browser-assert": { @@ -12732,6 +13201,17 @@ "dev": true, "license": "(Apache-2.0 AND MIT)" }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "16.1.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", @@ -13188,9 +13668,9 @@ } }, "node_modules/chromatic": { - "version": "11.20.2", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.20.2.tgz", - "integrity": "sha512-c+M3HVl5Y60c7ipGTZTyeWzWubRW70YsJ7PPDpO1D735ib8+Lu3yGF90j61pvgkXGngpkTPHZyBw83lcu2JMxA==", + "version": "11.25.2", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz", + "integrity": "sha512-/9eQWn6BU1iFsop86t8Au21IksTRxwXAl7if8YHD05L2AbuMjClLWZo5cZojqrJHGKDhTqfrC2X2xE4uSm0iKw==", "dev": true, "license": "MIT", "bin": { @@ -14002,9 +14482,9 @@ } }, "node_modules/core-js": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", - "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", + "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -15205,9 +15685,9 @@ } }, "node_modules/electron": { - "version": "33.3.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.3.1.tgz", - "integrity": "sha512-Z7l2bVgpdKxHQMI4i0CirBX2n+iCYKOx5mbzNM3BpOyFELwlobEXKmzCmEnwP+3EcNeIhUQyIEBFQxN06QgdIw==", + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-34.0.0.tgz", + "integrity": "sha512-fpaPb0lifoUJ6UJa4Lk8/0B2Ku/xDZWdc1Gkj67jbygTCrvSon0qquju6Ltx1Kz23GRqqlIHXiy9EvrjpY7/Wg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -15433,9 +15913,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.12.tgz", - "integrity": "sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==", + "version": "20.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.14.tgz", + "integrity": "sha512-w6qdYetNL5KRBiSClK/KWai+2IMEJuAj+EujKCumalFOwXtvOXaEan9AuwcRID2IcOIAWSIfR495hBtgKlx2zg==", "dev": true, "license": "MIT", "dependencies": { @@ -15543,8 +16023,21 @@ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "engines": { + "node": ">=6" + } + }, + "node_modules/envify": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/envify/-/envify-4.1.0.tgz", + "integrity": "sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw==", + "license": "MIT", + "dependencies": { + "esprima": "^4.0.0", + "through": "~2.3.4" + }, + "bin": { + "envify": "bin/envify" } }, "node_modules/envinfo": { @@ -15686,9 +16179,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -15945,13 +16438,13 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", "dev": true, "license": "MIT", "bin": { - "eslint-config-prettier": "bin/cli.js" + "eslint-config-prettier": "build/bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" @@ -16209,9 +16702,9 @@ } }, "node_modules/eslint-plugin-tailwindcss": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.5.tgz", - "integrity": "sha512-8Mi7p7dm+mO1dHgRHHFdPu4RDTBk69Cn4P0B40vRQR+MrguUpwmKwhZy1kqYe3Km8/4nb+cyrCF+5SodOEmaow==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.18.0.tgz", + "integrity": "sha512-PQDU4ZMzFH0eb2DrfHPpbgo87Zgg2EXSMOj1NSfzdZm+aJzpuwGerfowMIaVehSREEa0idbf/eoNYAOHSJoDAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16404,7 +16897,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -16622,6 +17114,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -22017,9 +22520,9 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.0.tgz", - "integrity": "sha512-UdODqEZiQimd7rCzZ2vqFuELRNUda3mdv7M93jhE4SmDiqAj/w/msvwKgagH23jv2iCPw6Q5m+ltX4VlHvp2LQ==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.1.tgz", + "integrity": "sha512-P8yJuVRyLrm5KxCtFx+gjI5Bil+wO7wnTl7C3bXhvtTaAFGirzeB24++D0wGoUwxrUKecNiehemgCob9YL39NA==", "dev": true, "license": "MIT", "dependencies": { @@ -22458,6 +22961,13 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -22798,9 +23308,9 @@ } }, "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", "dev": true, "license": "MIT" }, @@ -24499,9 +25009,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -24575,18 +25085,6 @@ "node": ">= 10" } }, - "node_modules/ngx-infinite-scroll": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-18.0.0.tgz", - "integrity": "sha512-D183TDwpsd9Zl56UmItsl3RzHdN25srAISfg6lc3A8mEKkEgOq0s7ZzRAYcx8DHsAkMgtZqjIPEvMifD3DOB/g==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": ">=18.0.0 <19.0.0", - "@angular/core": ">=18.0.0 <19.0.0" - } - }, "node_modules/ngx-toastr": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz", @@ -26303,6 +26801,14 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", @@ -26625,9 +27131,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "dev": true, "funding": [ { @@ -26645,7 +27151,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -26929,9 +27435,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz", - "integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==", + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.10.tgz", + "integrity": "sha512-ndj2WLDaMzACnr1gAYZiZZLs5ZdOeBYgOsbBmHj3nvW/6q8h8PymsXiEnKvj/9qgCCAoHyvLOisoQdIcsDvIgw==", "dev": true, "license": "MIT", "engines": { @@ -26942,7 +27448,7 @@ "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", - "@zackad/prettier-plugin-twig-melody": "*", + "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", @@ -26969,7 +27475,7 @@ "@trivago/prettier-plugin-sort-imports": { "optional": true }, - "@zackad/prettier-plugin-twig-melody": { + "@zackad/prettier-plugin-twig": { "optional": true }, "prettier-plugin-astro": { @@ -28494,9 +29000,9 @@ } }, "node_modules/sass": { - "version": "1.83.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", - "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", + "version": "1.83.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", + "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", "dev": true, "license": "MIT", "dependencies": { @@ -28579,13 +29085,13 @@ "license": "MIT" }, "node_modules/sass/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -29055,6 +29561,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -29451,6 +29965,14 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -29470,6 +29992,14 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/steno": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", @@ -29480,13 +30010,13 @@ } }, "node_modules/storybook": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.4.7.tgz", - "integrity": "sha512-RP/nMJxiWyFc8EVMH5gp20ID032Wvk+Yr3lmKidoegto5Iy+2dVQnUoElZb2zpbVXNHWakGuAkfI0dY1Hfp/vw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.5.2.tgz", + "integrity": "sha512-pf84emQ7Pd5jBdT2gzlNs4kRaSI3pq0Lh8lSfV+YqIVXztXIHU+Lqyhek2Lhjb7btzA1tExrhJrgQUsIji7i7A==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core": "8.4.7" + "@storybook/core": "8.5.2" }, "bin": { "getstorybook": "bin/index.cjs", @@ -30387,6 +30917,22 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/tinyglobby": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", @@ -30401,6 +30947,17 @@ "node": ">=12.0.0" } }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/tinyrainbow": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", @@ -30422,21 +30979,21 @@ } }, "node_modules/tldts": { - "version": "6.1.71", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.71.tgz", - "integrity": "sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw==", + "version": "6.1.74", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.74.tgz", + "integrity": "sha512-O5vTZ1UmmEmrLl/59U9igitnSMlprALLaLgbv//dEvjobPT9vyURhHXKMCDLEhn3qxZFIkb9PwAfNYV0Ol7RPQ==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.71" + "tldts-core": "^6.1.74" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.71", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.71.tgz", - "integrity": "sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==", + "version": "6.1.74", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.74.tgz", + "integrity": "sha512-gTwtY6L2GfuxiL4CWpLknv9JDYYqBvKCk/BT5uAaAvCA0s6pzX7lr2IrkQZSUlnSjRHIjTl8ZwKCVXJ7XNRWYw==", "license": "MIT" }, "node_modules/tmp": { @@ -30667,9 +31224,9 @@ } }, "node_modules/ts-loader": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", - "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -31151,6 +31708,51 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.20.0.tgz", + "integrity": "sha512-Kxz2QRFsgbWj6Xcftlw3Dd154b3cEPFqQC+qMZrMypSijPd4UanKKvoKDrJ4o8AIfZFKAF+7sMaEIR8mTElozA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.20.0", + "@typescript-eslint/parser": "8.20.0", + "@typescript-eslint/utils": "8.20.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/typescript-strict-plugin": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/typescript-strict-plugin/-/typescript-strict-plugin-2.4.4.tgz", @@ -31521,9 +32123,9 @@ } }, "node_modules/unplugin": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.0.tgz", - "integrity": "sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", "dev": true, "license": "MIT", "dependencies": { @@ -31848,6 +32450,30 @@ } } }, + "node_modules/vite-node": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.4.tgz", + "integrity": "sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.2", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", @@ -31902,6 +32528,191 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vitest": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.4.tgz", + "integrity": "sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "3.0.4", + "@vitest/mocker": "3.0.4", + "@vitest/pretty-format": "^3.0.4", + "@vitest/runner": "3.0.4", + "@vitest/snapshot": "3.0.4", + "@vitest/spy": "3.0.4", + "@vitest/utils": "3.0.4", + "chai": "^5.1.2", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.4", + "@vitest/ui": "3.0.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest-axe": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vitest-axe/-/vitest-axe-0.1.0.tgz", + "integrity": "sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.0.0", + "axe-core": "^4.4.2", + "chalk": "^5.0.1", + "dom-accessibility-api": "^0.5.14", + "lodash-es": "^4.17.21", + "redent": "^3.0.0" + }, + "peerDependencies": { + "vitest": ">=0.16.0" + } + }, + "node_modules/vitest-axe/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/vitest/node_modules/@vitest/expect": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.4.tgz", + "integrity": "sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/spy": "3.0.4", + "@vitest/utils": "3.0.4", + "chai": "^5.1.2", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/pretty-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", + "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/spy": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.4.tgz", + "integrity": "sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", + "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/pretty-format": "3.0.4", + "loupe": "^3.1.2", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/vitest/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -31915,13 +32726,13 @@ } }, "node_modules/wait-on": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.1.tgz", - "integrity": "sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.2.tgz", + "integrity": "sha512-qHlU6AawrgAIHlueGQHQ+ETcPLAauXbnoTKl3RKq20W0T8x0DKVAo5xWIYjHSyvHxQlcYbFdR0jp4T9bDVITFA==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.7.7", + "axios": "^1.7.9", "joi": "^17.13.3", "lodash": "^4.17.21", "minimist": "^1.2.8", @@ -32097,6 +32908,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", "integrity": "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==", "dev": true, + "license": "MIT", "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.12", @@ -32593,6 +33405,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index 8b692a57ac92..5145b058f6ff 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,7 @@ ], "devDependencies": { "@angular-devkit/build-angular": "18.2.12", - "@angular-eslint/eslint-plugin": "18.4.3", - "@angular-eslint/eslint-plugin-template": "18.4.3", "@angular-eslint/schematics": "18.4.3", - "@angular-eslint/template-parser": "18.4.3", "@angular/cli": "18.2.12", "@angular/compiler-cli": "18.2.13", "@babel/core": "7.24.9", @@ -49,16 +46,16 @@ "@electron/notarize": "2.5.0", "@electron/rebuild": "3.7.1", "@ngtools/webpack": "18.2.12", - "@storybook/addon-a11y": "8.4.7", - "@storybook/addon-actions": "8.4.7", + "@storybook/addon-a11y": "8.5.2", + "@storybook/addon-actions": "8.5.2", "@storybook/addon-designs": "8.0.4", - "@storybook/addon-essentials": "8.4.7", - "@storybook/addon-interactions": "8.4.7", - "@storybook/addon-links": "8.4.7", - "@storybook/angular": "8.4.7", - "@storybook/manager-api": "8.4.7", - "@storybook/theming": "8.4.7", - "@storybook/web-components-webpack5": "8.4.7", + "@storybook/addon-essentials": "8.5.2", + "@storybook/addon-interactions": "8.5.2", + "@storybook/addon-links": "8.5.2", + "@storybook/angular": "8.5.2", + "@storybook/manager-api": "8.5.2", + "@storybook/theming": "8.5.2", + "@storybook/web-components-webpack5": "8.5.2", "@types/argon2-browser": "1.18.4", "@types/chrome": "0.0.280", "@types/firefox-webext-browser": "120.0.4", @@ -73,7 +70,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.5", + "@types/node": "22.10.7", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -81,33 +78,34 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/eslint-plugin": "8.20.0", - "@typescript-eslint/parser": "8.20.0", + "@typescript-eslint/rule-tester": "8.22.0", + "@typescript-eslint/utils": "8.22.0", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", + "angular-eslint": "18.4.3", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", - "chromatic": "11.20.2", + "chromatic": "11.25.2", "concurrently": "9.1.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.3.1", + "electron": "34.0.0", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", "electron-updater": "6.3.9", "eslint": "8.57.1", - "eslint-config-prettier": "9.1.0", + "eslint-config-prettier": "10.0.1", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-storybook": "0.11.2", - "eslint-plugin-tailwindcss": "3.17.5", + "eslint-plugin-tailwindcss": "3.18.0", "html-loader": "5.1.0", "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.3", @@ -116,30 +114,31 @@ "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", - "lint-staged": "15.4.0", + "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-loader": "8.1.1", "prettier": "3.4.2", - "prettier-plugin-tailwindcss": "0.6.9", + "prettier-plugin-tailwindcss": "0.6.10", "process": "0.11.10", "remark-gfm": "4.0.0", "rimraf": "6.0.1", - "sass": "1.83.1", + "sass": "1.83.4", "sass-loader": "16.0.4", - "storybook": "8.4.7", + "storybook": "8.5.2", "style-loader": "4.0.0", "tailwindcss": "3.4.17", "ts-jest": "29.2.2", - "ts-loader": "9.5.1", + "ts-loader": "9.5.2", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", "typescript": "5.4.2", - "typescript-strict-plugin": "^2.4.4", + "typescript-eslint": "8.20.0", + "typescript-strict-plugin": "2.4.4", "url": "0.11.4", "util": "0.12.5", - "wait-on": "8.0.1", + "wait-on": "8.0.2", "webpack": "5.97.1", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.0", @@ -167,12 +166,12 @@ "argon2-browser": "1.18.0", "big-integer": "1.6.52", "bootstrap": "4.6.0", - "braintree-web-drop-in": "1.43.0", + "braintree-web-drop-in": "1.44.0", "buffer": "6.0.3", "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.39.0", + "core-js": "3.40.0", "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", @@ -186,7 +185,6 @@ "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "1.4.5-lts.1", - "ngx-infinite-scroll": "18.0.0", "ngx-toastr": "19.0.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", @@ -201,7 +199,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.71", + "tldts": "6.1.74", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" @@ -211,6 +209,9 @@ "@storybook/angular": { "zone.js": "$zone.js" }, + "react": "18.3.1", + "react-dom": "18.3.1", + "@types/react": "18.3.1", "replacestream": "4.0.3" }, "lint-staged": { diff --git a/scripts/.eslintrc.json b/scripts/.eslintrc.json deleted file mode 100644 index 10d223883786..000000000000 --- a/scripts/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "node": true - } -} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index a69452389f54..611b30a3bdb6 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -20,6 +20,7 @@ "@bitwarden/angular/*": ["./libs/angular/src/*"], "@bitwarden/auth": ["./libs/auth/src"], "@bitwarden/billing": ["./libs/billing/src"], + "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/generator-components": ["./libs/tools/generator/components/src"], @@ -27,18 +28,18 @@ "@bitwarden/generator-history": ["./libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["./libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["./libs/tools/generator/extensions/navigation/src"], - "@bitwarden/vault-export-core": [".libs/tools/export/vault-export/vault-export-core/src"], - "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], - "@bitwarden/importer/core": ["./libs/importer/src"], - "@bitwarden/importer/ui": ["./libs/importer/src/components"], + "@bitwarden/importer-core": ["./libs/importer/src"], + "@bitwarden/importer-ui": ["./libs/importer/src/components"], + "@bitwarden/key-management": ["./libs/key-management/src"], + "@bitwarden/key-management-ui": ["./libs/key-management-ui/src/index,ts"], + "@bitwarden/node/*": ["./libs/node/src/*"], + "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/send-ui": [".libs/tools/send/send-ui/src"], "@bitwarden/tools-card": [".libs/tools/card/src"], - "@bitwarden/platform": ["./libs/platform/src"], - "@bitwarden/node/*": ["./libs/node/src/*"], - "@bitwarden/vault": ["./libs/vault/src"], - "@bitwarden/key-management": ["./libs/key-management/src"], - "@bitwarden/key-management/angular": ["./libs/key-management/src/angular"], - "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"] + "@bitwarden/ui-common": ["./libs/ui/common/src"], + "@bitwarden/vault-export-core": [".libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], + "@bitwarden/vault": ["./libs/vault/src"] }, "plugins": [ { @@ -46,6 +47,11 @@ } ] }, + "files": [ + ".storybook/main.ts", + ".storybook/manager.js", + "apps/browser/src/autofill/content/components/.lit-storybook/main.ts" + ], "include": ["apps/**/*", "libs/**/*", "bitwarden_license/**/*", "scripts/**/*"], "exclude": ["**/build", "**/dist"] } diff --git a/tsconfig.json b/tsconfig.json index 91b4ee7dd6b0..e6e4c47096b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,9 +18,10 @@ "paths": { "@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"], + "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/billing": ["./libs/billing/src"], + "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/generator-components": ["./libs/tools/generator/components/src"], @@ -28,19 +29,19 @@ "@bitwarden/generator-history": ["./libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["./libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["./libs/tools/generator/extensions/navigation/src"], - "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], - "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], - "@bitwarden/importer/core": ["./libs/importer/src"], - "@bitwarden/importer/ui": ["./libs/importer/src/components"], + "@bitwarden/importer-core": ["./libs/importer/src"], + "@bitwarden/importer-ui": ["./libs/importer/src/components"], "@bitwarden/key-management": ["./libs/key-management/src"], - "@bitwarden/key-management/angular": ["./libs/key-management/src/angular"], + "@bitwarden/key-management-ui": ["./libs/key-management-ui/src"], + "@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["./libs/tools/card/src"], - "@bitwarden/node/*": ["./libs/node/src/*"], - "@bitwarden/web-vault/*": ["./apps/web/src/*"], + "@bitwarden/ui-common": ["./libs/ui/common/src"], + "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/vault": ["./libs/vault/src"], - "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"] + "@bitwarden/web-vault/*": ["./apps/web/src/*"] }, "plugins": [ {