diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2665f345568..4b956fd577a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -131,6 +131,7 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev .github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev # ESLint custom rules libs/eslint @bitwarden/team-platform-dev +libs/eslint/components @bitwarden/team-ui-foundation # Typescript tooling tsconfig.base.json @bitwarden/team-platform-dev nx.json @bitwarden/team-platform-dev @@ -143,6 +144,7 @@ apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-dev apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofill-dev apps/desktop/desktop_native/autotype @bitwarden/team-autofill-dev +.github/workflows/test-browser-interactions.yml @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 diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 906bbbd7125..e646049c3d6 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -197,6 +197,7 @@ "nx", "oo7", "oslog", + "parse5", "pin-project", "pkg", "postcss", diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index be140b9a20e..43661d50910 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -232,7 +232,7 @@ jobs: npm --version - name: Download browser source - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: browser-source-${{ env._BUILD_NUMBER }}.zip diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 73b765f207a..22ba3a3e7be 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -499,7 +499,7 @@ jobs: echo "BW Package Version: $_PACKAGE_VERSION" - name: Get bw linux cli - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: bw-linux-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/snap diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 366d439fb45..e6c77b366b1 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1079,7 +1079,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -1344,7 +1344,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: path: ${{ github.workspace }}/browser-build-artifacts diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index e3eb9090cb7..eb6af20f9ee 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -1035,7 +1035,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Download all artifacts - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: path: apps/desktop/artifacts diff --git a/.github/workflows/test-browser-interactions.yml b/.github/workflows/test-browser-interactions.yml new file mode 100644 index 00000000000..c6427b2e0d8 --- /dev/null +++ b/.github/workflows/test-browser-interactions.yml @@ -0,0 +1,83 @@ +name: Autofill BIT checks +run-name: Autofill BIT checks on ${{ github.event.workflow_run.head_branch }} build + +on: + workflow_run: + workflows: ["Build Browser"] + types: + - completed + +jobs: + check-files: + name: Check files + runs-on: ubuntu-22.04 + permissions: + actions: write + contents: read + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Check for job requirements + if: ${{ !github.event.workflow_run.pull_requests || !github.event.workflow_run.head_branch }} + env: + GH_TOKEN: ${{ github.token }} + run: | + gh run cancel ${{ github.run_id }} + gh run watch ${{ github.run_id }} + + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-org-bitwarden + secrets: "BW-GHAPP-ID,BW-GHAPP-KEY" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + + - name: Generate GH App token + uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3 + id: app-token + with: + app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} + private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} + owner: bitwarden + repositories: browser-interactions-testing + permission-actions: write + + - name: Get changed files + id: changed-files + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + list-files: shell + ref: ${{ github.event.workflow_run.head_branch }} + token: ${{ secrets.GITHUB_TOKEN }} + filters: | + monitored: + - 'apps/browser/src/autofill/**' + - 'apps/browser/src/background/**' + - 'apps/browser/src/platform/services/browser-script-injector.service.ts' + + - name: Trigger test-all workflow in browser-interactions-testing + if: steps.changed-files.outputs.monitored == 'true' + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + with: + token: ${{ steps.app-token.outputs.token }} + repository: "bitwarden/browser-interactions-testing" + event-type: trigger-bit-tests + client-payload: |- + { + "origin_issue": ${{ github.event.workflow_run.pull_requests[0].number }}, + "origin_branch": "${{ github.event.workflow_run.pull_requests[0].head.ref }}" + } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8bfd368884..64c4e0dff13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -170,13 +170,13 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Download jest coverage - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: jest-coverage path: ./ - name: Download rust coverage - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: rust-coverage path: ./apps/desktop/desktop_native diff --git a/apps/browser/package.json b/apps/browser/package.json index cb0de28df5b..46c92ca8902 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.7.1", + "version": "2025.8.0", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 65ee9cab458..ee723217f2a 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 16b74ffe175..1d26f50bca6 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -14,7 +14,7 @@ "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { - "message": "Güvənli seyfinizə müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." + "message": "Güvənli seyfinizə erişmək üçün giriş edin və ya yeni bir hesab yaradın." }, "inviteAccepted": { "message": "Dəvət qəbul edildi" @@ -59,7 +59,7 @@ "message": "Ana parol" }, "masterPassDesc": { - "message": "Ana parol, seyfinizə müraciət etmək üçün istifadə edəcəyiniz paroldur. Ana parolu yadda saxlamaq çox vacibdir. Unutsanız, parolu bərpa etməyin heç bir yolu yoxdur." + "message": "Ana parol, seyfinizə erişmək üçün istifadə edəcəyiniz paroldur. Ana parolu yadda saxlamaq çox vacibdir. Unutsanız, parolu bərpa etməyin heç bir yolu yoxdur." }, "masterPassHintDesc": { "message": "Ana parol məsləhəti, unutduğunuz parolunuzu xatırlamağınıza kömək edir." @@ -350,7 +350,7 @@ "message": "Bitwarden Sirr Meneceri" }, "continueToSecretsManagerPageDesc": { - "message": "Bitwarden Sirr Meneceri ilə developer sirlərini güvənli bir şəkildə saxlayın, idarə edin və paylaşın. Daha ətraflı bitwarden.com veb saytında öyrənə bilərsiniz." + "message": "Bitwarden Sirr Meneceri ilə gəlişdirici sirlərini güvənli bir şəkildə saxlayın, idarə edin və paylaşın. Daha ətraflı bitwarden.com veb saytında öyrənə bilərsiniz." }, "passwordlessDotDev": { "message": "Passwordless.dev" @@ -548,7 +548,7 @@ "message": "Seyfdə axtar" }, "resetSearch": { - "message": "Reset search" + "message": "Axtarışı sıfırla" }, "edit": { "message": "Düzəliş et" @@ -1216,10 +1216,10 @@ "message": "Konteks menyu seçimlərini göstər" }, "contextMenuItemDesc": { - "message": "Veb sayt üçün parol yaratmaq və uyuşan giriş məlumatlarına müraciət etmək üçün sağ klikləməni istifadə edin." + "message": "Veb sayt üçün parol yaradılmasına və uyuşan giriş məlumatlarına erişmək üçün sağ klikləməni istifadə edin." }, "contextMenuItemDescAlt": { - "message": "Veb sayt üçün parol yaratmaq və uyuşan giriş məlumatlarına müraciət etmək üçün sağ klikləməni istifadə edin. Giriş etmiş bütün hesablara aiddir." + "message": "Veb sayt üçün parol yaradılmasına və uyuşan giriş məlumatlarına erişmək üçün sağ klikləməni istifadə edin. Giriş etmiş bütün hesablara aiddir." }, "defaultUriMatchDetection": { "message": "İlkin URI uyuşma aşkarlaması", @@ -1290,16 +1290,16 @@ "message": "Seyfi xaricə köçürməyi təsdiqlə" }, "exportWarningDesc": { - "message": "Xaricə köçürdüyünüz bu fayldakı datanız şifrələnməmiş formatdadır. Bu faylı güvənli olmayan kanallar (e-poçt kimi) üzərində saxlamamalı və ya göndərməməlisiniz. İşiniz bitdikdən sonra faylı dərhal silin." + "message": "Bu xaricə köçürmədəki seyf veriləriniz şifrələnməmiş formatdadır. Bu xaricə köçürülmüş faylı güvənli olmayan kanallar (e-poçt kimi) üzərində saxlamamalı və ya göndərməməlisiniz. İşiniz bitdikdən sonra faylı dərhal silin." }, "encExportKeyWarningDesc": { - "message": "Xaricə köçürdüyünüz bu fayldakı data, hesabınızın şifrələmə açarı istifadə edilərək şifrələnir. Hesabınızın şifrələmə açarını dəyişdirsəniz, bu faylın şifrəsini aça bilməyəcəksiniz və onu yenidən xaricə köçürməli olacaqsınız." + "message": "Bu xaricə köçürmə, verilərinizi hesabınızın şifrələmə açarını istifadə edərək şifrələyir. Hesabınızın şifrələmə açarını dəyişdirsəniz, bu faylın şifrəsini aça bilməyəcəksiniz və onu yenidən xaricə köçürməli olacaqsınız." }, "encExportAccountWarningDesc": { "message": "Hesab şifrələmə açarları, hər Bitwarden istifadəçi hesabı üçün unikaldır, buna görə də şifrələnmiş bir xaricə köçürməni, fərqli bir hesaba köçürə bilməzsiniz." }, "exportMasterPassword": { - "message": "Seyf datanızı xaricə köçürmək üçün ana parolunuzu daxil edin." + "message": "Seyf verilərinizi xaricə köçürmək üçün ana parolunuzu daxil edin." }, "shared": { "message": "Paylaşılan" @@ -1399,13 +1399,13 @@ "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş anbar sahəsi" }, "premiumSignUpEmergency": { - "message": "Fövqəladə hal müraciəti" + "message": "Fövqəladə hal erişimi." }, "premiumSignUpTwoStepOptions": { "message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri." }, "ppremiumSignUpReports": { - "message": "Seyfinizi güvəndə saxlamaq üçün parol gigiyenası, hesab sağlamlığı və data pozuntusu hesabatları." + "message": "Seyfinizi güvəndə saxlamaq üçün parol gigiyenası, hesab sağlamlığı və veri pozuntusu hesabatları." }, "ppremiumSignUpTotp": { "message": "Seyfinizdəki girişlər üçün TOTP doğrulama kodu (2FA) yaradıcısı." @@ -1523,7 +1523,7 @@ "message": "İki addımlı giriş üsulunu seçin" }, "recoveryCodeDesc": { - "message": "İki faktorlu provayderlərinə müraciəti itirmisiniz? Geri qaytarma kodunuzu istifadə edərək hesabınızdakı bütün iki faktorlu provayderləri sıradan çıxarda bilərsiniz." + "message": "İki faktorlu provayderlərinizə erişə bilmirsiniz? Geri qaytarma kodunuzu istifadə edərək hesabınızdakı bütün iki faktorlu provayderləri söndürün." }, "recoveryCodeTitle": { "message": "Geri qaytarma kodu" @@ -1539,7 +1539,7 @@ "message": "Yubico OTP Güvənlik Açarı" }, "yubiKeyDesc": { - "message": "Hesabınıza müraciət etmək üçün bir YubiKey istifadə edin. YubiKey 4, 4 Nano, 4C və NEO cihazları ilə işləyir." + "message": "Hesabınıza erişmək üçün bir YubiKey istifadə edin. YubiKey 4, 4 Nano, 4C və NEO cihazları ilə işləyir." }, "duoDescV2": { "message": "Duo Security tərəfindən yaradılan kodu daxil edin.", @@ -1553,7 +1553,7 @@ "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Hesabınıza müraciət etmək üçün WebAuthn özəllikli istənilən bir güvənlik açarı istifadə edin." + "message": "Hesabınıza erişmək üçün istənilən WebAuthn uyumlu güvənlik açarını istifadə edin." }, "emailTitle": { "message": "E-poçt" @@ -2023,7 +2023,7 @@ "message": "Parolun ifşalanıb ifşalanmadığını yoxlayın." }, "passwordExposed": { - "message": "Bu parol, məlumat pozuntularında $VALUE$ dəfə üzə çıxıb. Dəyişdirməyi məsləhət görürük.", + "message": "Bu parol, veri pozuntularında $VALUE$ dəfə üzə çıxıb. Dəyişdirməyi məsləhət görürük.", "placeholders": { "value": { "content": "$1", @@ -2032,7 +2032,7 @@ } }, "passwordSafe": { - "message": "Bu parol, məlumat pozuntularında qeydə alınmayıb. Rahatlıqla istifadə edə bilərsiniz." + "message": "Bu parol, veri pozuntularında qeydə alınmayıb. Rahatlıqla istifadə edə bilərsiniz." }, "baseDomain": { "message": "Baza domeni", @@ -2275,7 +2275,7 @@ "message": "Artıq hesabınız var?" }, "vaultTimeoutLogOutConfirmation": { - "message": "Çıxış etdikdə, seyfinizə bütün müraciətiniz dayanacaq və vaxt bitməsindən sonra onlayn kimlik doğrulaması tələb olunacaq. Bu ayarı istifadə etmək istədiyinizə əminsiniz?" + "message": "Çıxış etdikdə, seyfinizə erişiminiz tamamilə dayanacaq və vaxt bitdikdən sonra onlayn kimlik doğrulaması tələb olunacaq. Bu ayarı istifadə etmək istədiyinizə əminsiniz?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "Vaxt bitmə əməliyyat təsdiqi" @@ -2401,7 +2401,7 @@ "message": "Oldu" }, "errorRefreshingAccessToken": { - "message": "Müraciət tokeni təzələmə xətası" + "message": "Erişim tokeni təzələmə xətası" }, "errorRefreshingAccessTokenDesc": { "message": "Təzələmə tokeni və ya API açarlar tapılmadı. Lütfən çıxış edib yenidən giriş etməyə çalışın." @@ -2726,7 +2726,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCountReached": { - "message": "Maksimal müraciət sayına çatıldı", + "message": "Maksimum erişim sayına çatıldı", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "hideTextByDefault": { @@ -2810,7 +2810,7 @@ "message": "Özəl" }, "sendPasswordDescV3": { - "message": "Alıcıların bu \"Send\"ə müraciət etməsi üçün ixtiyari bir parol əlavə edin.", + "message": "Alıcıların bu \"Send\"ə erişməsi üçün ixtiyari bir parol əlavə edin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { @@ -2941,13 +2941,13 @@ "message": "Ana parolu güncəllə" }, "updateMasterPasswordWarning": { - "message": "Ana parolunuz təzəlikcə təşkilatınızdakı bir administrator tərəfindən dəyişdirildi. Seyfə müraciət etmək üçün onu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış edəcəksiniz və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." + "message": "Ana parolunuz təzəlikcə təşkilatınızdakı bir inzibatçı tərəfindən dəyişdirildi. Seyfə erişmək üçün onu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış edəcəksiniz və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." }, "updateWeakMasterPasswordWarning": { - "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Seyfə müraciət üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." + "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Seyfə erişmək üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." }, "tdeDisabledMasterPasswordRequired": { - "message": "Təşkilatınız, güvənli cihaz şifrələməsini sıradan çıxartdı. Seyfinizə müraciət etmək üçün lütfən ana parol təyin edin." + "message": "Təşkilatınız, güvənli cihaz şifrələməsini sıradan çıxartdı. Seyfinizə erişmək üçün lütfən ana parol təyin edin." }, "resetPasswordPolicyAutoEnroll": { "message": "Avtomatik yazılma" @@ -3145,7 +3145,7 @@ "message": "Bitwarden, aşağıda sadalanan seyf element(lər)inin şifrəsini aça bilmədi." }, "contactCSToAvoidDataLossPart1": { - "message": "Əlavə data itkisini önləmək üçün", + "message": "Əlavə veri itkisini önləmək üçün", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -3362,7 +3362,7 @@ "description": "Part of a URL." }, "apiAccessToken": { - "message": "API müraciət tokeni" + "message": "API erişim tokeni" }, "apiKey": { "message": "API açar" @@ -3377,7 +3377,7 @@ "message": "Təşkilat dayandırıldı." }, "disabledOrganizationFilterError": { - "message": "Dayandırılmış Təşkilatlardakı elementlərə müraciət edilə bilmir. Kömək üçün Təşkilatınızın sahibi ilə əlaqə saxlayın." + "message": "Fəaliyyəti dayandırılmış Təşkilatlardakı elementlərə erişilə bilməz. Kömək üçün Təşkilatınızın sahibi ilə əlaqə saxlayın." }, "loggingInTo": { "message": "$DOMAIN$ domeninə giriş edilir", @@ -3495,16 +3495,16 @@ "message": "İfşa olunmuş ana parol" }, "exposedMasterPasswordDesc": { - "message": "Parol, məlumat pozuntusunda tapıldı. Hesabınızı qorumaq üçün unikal bir parol istifadə edin. İfşa olunmuş bir parol istifadə etmək istədiyinizə əminsiniz?" + "message": "Parol, veri pozuntusunda tapıldı. Hesabınızı qorumaq üçün unikal bir parol istifadə edin. İfşa olunmuş bir parol istifadə etmək istədiyinizə əminsiniz?" }, "weakAndExposedMasterPassword": { "message": "Zəif və ifşa olunmuş ana parol" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Zəif parol məlumat pozuntusunda aşkarlandı və tapıldı. Hesabınızı qorumaq üçün güclü və unikal bir parol istifadə edin. Bu parolu istifadə etmək istədiyinizə əminsiniz?" + "message": "Zəif parol, veri pozuntusunda aşkarlandı və tapıldı. Hesabınızı qorumaq üçün güclü və unikal bir parol istifadə edin. Bu parolu istifadə etmək istədiyinizə əminsiniz?" }, "checkForBreaches": { - "message": "Bu parol üçün bilinən məlumat pozuntularını yoxlayın" + "message": "Bu parol üçün bilinən veri pozuntularını yoxlayın" }, "important": { "message": "Vacib:" @@ -3631,7 +3631,7 @@ "message": "Cihazlar" }, "accessAttemptBy": { - "message": "$EMAIL$ ilə müraciət cəhdi", + "message": "$EMAIL$ ilə erişim cəhdi", "placeholders": { "email": { "content": "$1", @@ -3640,10 +3640,10 @@ } }, "confirmAccess": { - "message": "Müraciəti təsdiqlə" + "message": "Erişimi təsdiqlə" }, "denyAccess": { - "message": "Müraciətə rədd cavabı ver" + "message": "Erişimə rədd cavabı ver" }, "time": { "message": "Vaxt" @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Bu tələb artıq yararsızdır." }, - "areYouTryingToAccessYourAccount": { - "message": "Hesabınıza müraciət etməyə çalışırsınız?" - }, - "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." }, @@ -3747,7 +3728,7 @@ "description": "European Union" }, "accessDenied": { - "message": "Müraciət rədd edildi. Bu səhifəyə baxmaq üçün icazəniz yoxdur." + "message": "Erişim rədd edildi. Bu səhifəyə baxmaq üçün icazəniz yoxdur." }, "general": { "message": "Ümumi" @@ -3792,7 +3773,7 @@ "message": "Təşkilata güvənilmir" }, "emergencyAccessTrustWarning": { - "message": "Hesabınızın təhlükəsizliyi üçün yalnız bu istifadəçiyə fövqəladə hal müraciəti icazəsini verdiyinizi və onun barmaq izinin hesabında görünən barmaq izi ilə uyuşduğunu təsdiqləyin" + "message": "Hesabınızın təhlükəsizliyi üçün yalnız bu istifadəçiyə fövqəladə hal erişim icazəsini verdiyinizi və onun barmaq izinin hesabında görünən barmaq izi ilə uyuşduğunu təsdiqləyin" }, "orgTrustWarning": { "message": "Hesabınızın təhlükəsizliyi üçün yalnız bu təşkilatın üzvüsünüzsə, hesab geri qaytarma fəaldırsa və aşağıda görünən barmaq izi təşkilatın barmaq izi ilə uyuşursa davam edin." @@ -3808,7 +3789,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "İstənilən platformada faylları və dataları hər kəslə paylaşın. İfşa olunmağı məhdudlaşdıraraq məlumatlarınız ucdan-uca şifrələnmiş qalacaq.", + "message": "İstənilən platformada faylları və veriləri hər kəslə güvənli şəkildə paylaşın. Məlumatlarınız, ifşa olunmamaq üçün ucdan-uca şifrələnmiş qalacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -4039,14 +4020,14 @@ "message": "Yox say" }, "importData": { - "message": "Datanı daxilə köçür", + "message": "Veriləri daxilə köçür", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { "message": "Daxilə köçürmə xətası" }, "importErrorDesc": { - "message": "Daxilə köçürməyə çalışdığınız data ilə bağlı bir problem var. Lütfən mənbə faylınızda aşağıda sadalanan xətaları həll edib yenidən sınayın." + "message": "Daxilə köçürməyə çalışdığınız verilərlə bağlı bir problem var. Lütfən mənbə faylınızda aşağıda sadalanan xətaları həll edib yenidən sınayın." }, "resolveTheErrorsBelowAndTryAgain": { "message": "Aşağıdakı xətaları həll edin və yenidən sınayın." @@ -4055,7 +4036,7 @@ "message": "Açıqlama" }, "importSuccess": { - "message": "Data uğurla daxilə köçürüldü" + "message": "Verilər uğurla daxilə köçürüldü" }, "importSuccessNumberOfItems": { "message": "Cəmi $AMOUNT$ element daxilə köçürüldü.", @@ -4106,7 +4087,7 @@ "message": "Cəmi" }, "importWarning": { - "message": "Datanı $ORGANIZATION$ təşkilatına köçürürsünüz. Datanızı bu təşkilatın üzvləri ilə paylaşa bilərsiniz. Davam etmək istəyirsiniz?", + "message": "Veriləri $ORGANIZATION$ təşkilatına köçürürsünüz. Veriləriniz, bu təşkilatın üzvləri ilə paylaşıla bilər. Davam etmək istəyirsiniz?", "placeholders": { "organization": { "content": "$1", @@ -4127,13 +4108,13 @@ "message": "Duo-nu başlat" }, "importFormatError": { - "message": "Data doğru format edilməyib. Lütfən daxilə köçürmə faylınızı yoxlayıb yenidən sınayın." + "message": "Verilər düzgün format olunmayıb. Lütfən daxilə köçürmə faylınızı yoxlayıb yenidən sınayın." }, "importNothingError": { "message": "Heç nə daxilə köçürülmədi." }, "importEncKeyError": { - "message": "Xaricə köçürülən faylın şifrəsi açılarkən xəta baş verdi. Şifrələmə açarınız, datanı xaricə köçürmək üçün istifadə edilən şifrələmə açarı ilə uyuşmur." + "message": "Xaricə köçürülən faylın şifrəsi açılarkən xəta baş verdi. Şifrələmə açarınız, veriləri xaricə köçürmək üçün istifadə edilən şifrələmə açarı ilə uyuşmur." }, "invalidFilePassword": { "message": "Yararsız fayl parolu, lütfən xaricə köçürmə faylını yaradarkən daxil etdiyiniz parolu istifadə edin." @@ -4192,19 +4173,19 @@ "message": "Seyfi daxilə köçürməyi təsdiqlə" }, "confirmVaultImportDesc": { - "message": "Bu fayl parolla qorunur. Məlumatları daxilə köçürmək üçün fayl parolunu daxil edin." + "message": "Bu fayl parolla qorunur. Veriləri daxilə köçürmək üçün fayl parolunu daxil edin." }, "confirmFilePassword": { "message": "Fayl parolunu təsdiqlə" }, "exportSuccess": { - "message": "Seyf datası xaricə köçürüldü" + "message": "Seyf veriləri xaricə köçürüldü" }, "typePasskey": { "message": "Keçid açarı" }, "accessing": { - "message": "Müraciət edilir" + "message": "Erişilir" }, "loggedInExclamation": { "message": "Giriş edildi!" @@ -4270,7 +4251,7 @@ "message": "Çox faktorlu kimlik doğrulama ləğv edildi" }, "noLastPassDataFound": { - "message": "LastPass datası tapılmadı" + "message": "LastPass veriləri tapılmadı" }, "incorrectUsernameOrPassword": { "message": "Yanlış istifadəçi adı və ya parol" @@ -4376,7 +4357,7 @@ "message": "sahiblik edən" }, "useDeviceOrHardwareKey": { - "message": "Cihazınızı və ya avadanlıq açarınızı istifadə edin" + "message": "Cihazınızı və ya donanım açarınızı istifadə edin" }, "justOnce": { "message": "Yalnız bir dəfə" @@ -4700,7 +4681,7 @@ "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Deaktiv edilmiş təşkilatlardakı elementlərə müraciət edilə bilməz. Kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." + "message": "Deaktiv edilmiş təşkilatlardakı elementlərə erişilə bilməz. Kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." }, "additionalInformation": { "message": "Əlavə məlumat" @@ -4757,13 +4738,13 @@ "message": "Mobil tətbiqi əldə et" }, "getTheMobileAppDesc": { - "message": "Bitwarden mobil tətbiqi ilə parollarınıza hər yerdən müraciət edin." + "message": "Bitwarden mobil tətbiqi ilə parollarınıza hər yerdən erişin." }, "getTheDesktopApp": { "message": "Masaüstü tətbiqi əldə et" }, "getTheDesktopAppDesc": { - "message": "Seyfinizə brauzer olmadan müraciət edin, sonra həm masaüstü tətbiqində, həm də brauzer uzantısında kilid açma prosesini sürətləndirmək üçün biometrik ilə kilid açma prosesini qurun." + "message": "Seyfinizə brauzer olmadan erişin, sonra həm masaüstü tətbiqində, həm də brauzer uzantısında kilid açma prosesini sürətləndirmək üçün biometrik ilə kilid açma prosesini qurun." }, "downloadFromBitwardenNow": { "message": "İndi bitwarden.com saytından endir" @@ -4916,7 +4897,7 @@ "message": "Yüklənir" }, "data": { - "message": "Data" + "message": "Veri" }, "passkeys": { "message": "Keçid açarı", @@ -4934,10 +4915,10 @@ "message": "Təyin et" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementi görə biləcək." + "message": "Yalnız bu kolleksiyalara erişimi olan təşkilat üzvləri bu elementi görə biləcək." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementləri görə biləcək." + "message": "Yalnız bu kolleksiyalara erişimi olan təşkilat üzvləri bu elementləri görə biləcək." }, "bulkCollectionAssignmentWarning": { "message": "$TOTAL_COUNT$ element seçmisiniz. Düzəliş icazəniz olmadığı üçün $READONLY_COUNT$ elementi güncəlləyə bilməzsiniz.", @@ -4964,10 +4945,10 @@ "message": "Xana etiketi" }, "textHelpText": { - "message": "Təhlükəsizlik sualları kimi datalar üçün mətn xanalarını istifadə edin" + "message": "Təhlükəsizlik sualları kimi verilər üçün mətn xanalarını istifadə edin" }, "hiddenHelpText": { - "message": "Parol kimi həssas datalar üçün gizli xanaları istifadə edin" + "message": "Parol kimi həssas verilər üçün gizli xanaları istifadə edin" }, "checkBoxHelpText": { "message": "\"E-poçtu xatırla\" kimi formun təsdiq qutusunu avto-doldurmaq istəyirsinizsə təsdiq qutularını istifadə edin" @@ -5239,7 +5220,7 @@ "message": "Seyfinizin kilidini saniyələr ərzində açın" }, "unlockVaultDesc": { - "message": "Seyfinizə daha cəld müraciət etmək üçün kilid açma və bitmə vaxtı ayarlarını özəlləşdirə bilərsiniz." + "message": "Seyfinizə daha cəld erişmək üçün kilid açma və bitmə vaxtı ayarlarını özəlləşdirə bilərsiniz." }, "unlockPinSet": { "message": "PIN ilə kilid açma təyini" @@ -5491,7 +5472,7 @@ "message": "Bütün hesablarınız üçün güclü, unikal parollar yaratmaq və saxlamaq üçün yaradıcını istifadə edin." }, "secureDevices": { - "message": "Datanız, ehtiyacınız olan vaxt yanınızdadır" + "message": "Veriləriniz, ehtiyacınız olan vaxt yanınızdadır" }, "secureDevicesBody": { "message": "Bitwarden mobil, brauzer və masaüstü tətbiqləri ilə limitsiz cihaz arasında limitsiz parol saxlayın." @@ -5515,7 +5496,7 @@ "message": "Hazırkı səhifə üçün elementləri avto-doldur" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Asan müraciət üçün elementləri sevimlilərə əlavə et" + "message": "Asan erişmək üçün elementləri sevimlilərə əlavə et" }, "hasItemsVaultNudgeBodyThree": { "message": "Seyfinizdə başqa bir şey axtarın" @@ -5551,13 +5532,13 @@ "message": "Kimliklərinizlə, uzun qeydiyyat və ya əlaqə xanalarını daha tez avtomatik doldurun." }, "newNoteNudgeTitle": { - "message": "Həssas datalarınızı güvənli şəkildə saxlayın" + "message": "Həssas verilərinizi güvənli şəkildə saxlayın" }, "newNoteNudgeBody": { - "message": "Notlarla, bankçılıq və ya sığorta təfsilatları kimi həssas dataları təhlükəsiz saxlayın." + "message": "Notlarla, bankçılıq və ya sığorta təfsilatları kimi həssas veriləri təhlükəsiz saxlayın." }, "newSshNudgeTitle": { - "message": "Gəlişdirici dostu SSH müraciəti" + "message": "Gəlişdirici dostu SSH erişimi" }, "newSshNudgeBodyOne": { "message": "Açarlarınızı saxlayın və sürətli, şifrələnmiş kimlik doğrulama üçün SSH agentinə bağlayın.", @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly brauzerinizdə dəstəklənmir və ya fəal deyil. WebAssembly, Bitwarden tətbiqini istifadə etmək üçün tələb olunur.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Daha çox göstər" + }, + "showLess": { + "message": "Daha az göstər" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index d7a1db3adc8..d6090123878 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 79e13cdb677..3b411cd088c 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Тази заявка вече не е активна." }, - "areYouTryingToAccessYourAccount": { - "message": "Опитвате ли се да получите достъп до акаунта си?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "Вписването за $EMAIL$ на $DEVICE$ е одобрено", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Вие отказахте опит за вписване от друго устройство. Ако това наистина сте били Вие, опитайте да се впишете от устройството отново." - }, "loginRequestHasAlreadyExpired": { "message": "Заявката за вписване вече е изтекла." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly не е включено или не се поддържа от Вашия браузър. За ползването на приложението на Битуорден е необходимо WebAssembly да работи.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Показване на повече" + }, + "showLess": { + "message": "Показване на по-малко" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index a3c029fb963..55baca0dad2 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 8a94ba3e9e9..7ad375dabf0 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 42fb9c24003..3edfb98eba2 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 3f8dd2e2b48..916f765ea21 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -338,7 +338,7 @@ "message": "Pokračovat na bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden pro byznys" + "message": "Bitwarden pro podnikání" }, "bitwardenAuthenticator": { "message": "Autentifikátor Bitwarden" @@ -398,7 +398,7 @@ "message": "Název složky" }, "folderHintText": { - "message": "Vnořit složku přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra" + "message": "Složku vnoříte přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra" }, "noFoldersAdded": { "message": "Nebyly přidány žádné složky" @@ -407,7 +407,7 @@ "message": "Vytvořte složky pro organizaci Vašich položek trezoru" }, "deleteFolderPermanently": { - "message": "Opravdu chcete trvale smazat tuto složku?" + "message": "Opravdu chcete tuto složku trvale smazat?" }, "deleteFolder": { "message": "Smazat složku" @@ -531,10 +531,10 @@ "message": "Zahrnout číslice" }, "minNumbers": { - "message": "Minimální počet číslic" + "message": "Minimálně číslic" }, "minSpecial": { - "message": "Minimální počet speciálních znaků" + "message": "Minimálně speciálních znaků" }, "avoidAmbiguous": { "message": "Nepoužívat zaměnitelné znaky", @@ -1016,7 +1016,7 @@ "description": "This is the folder for uncategorized items" }, "enableAddLoginNotification": { - "message": "Ptát se na přidání přihlášení" + "message": "Zeptat se na přidání přihlášení" }, "vaultSaveOptionsTitle": { "message": "Uložit do voleb trezoru" @@ -1049,7 +1049,7 @@ "message": "Klepněte na položky pro automatické vyplnění v zobrazení trezoru" }, "clickToAutofill": { - "message": "Klepněte na položky v návrhu automatického vyplňování pro vyplnění" + "message": "Klepnout na položky v návrhu automatického vyplňování pro vyplnění" }, "clearClipboard": { "message": "Vymazat schránku", @@ -1305,7 +1305,7 @@ "message": "Sdílené" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden pro bvyznys Vám umožňuje sdílet položky v trezoru s ostatními prostřednictvím organizace. Více informací naleznete na bitwarden.com." + "message": "Bitwarden pro podnikání Vám umožňuje sdílet položky v trezoru s ostatními prostřednictvím organizace. Více informací naleznete na bitwarden.com." }, "moveToOrganization": { "message": "Přesunout do organizace" @@ -1660,7 +1660,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Automaticky vyplnit údaje při načtení stránky" + "message": "Automatické vyplnění údajů při načtení stránky" }, "enableAutoFillOnPageLoad": { "message": "Automaticky vyplnit údaje při načtení stránky" @@ -1678,7 +1678,7 @@ "message": "Více informací o automatickém vyplňování" }, "defaultAutoFillOnPageLoad": { - "message": "Výchozí nastavení automatického vyplňování pro položky přihlášení" + "message": "Výchozí nastavení autom. vyplňování" }, "defaultAutoFillOnPageLoadDesc": { "message": "Můžete vypnout automatické vyplňování při načtení stránky pro jednotlivé přihlašovací položky v zobrazení pro úpravu položky." @@ -1690,10 +1690,10 @@ "message": "Použít výchozí nastavení" }, "autoFillOnPageLoadYes": { - "message": "Automatické vyplnění při načtení stránky" + "message": "Automatické vyplnění při načtení" }, "autoFillOnPageLoadNo": { - "message": "Nevyplňovat automaticky při načtení stránky" + "message": "Nevyplňovat automaticky při načtení" }, "commandOpenPopup": { "message": "Otevřít vyskakovací okno trezoru" @@ -1851,7 +1851,7 @@ "message": "Slečna" }, "dr": { - "message": "MUDr." + "message": "Dr." }, "mx": { "message": "Neutrální" @@ -1875,7 +1875,7 @@ "message": "Společnost" }, "ssn": { - "message": "Číslo sociálního pojištění" + "message": "Rodné číslo" }, "passportNumber": { "message": "Číslo cestovního pasu" @@ -2099,7 +2099,7 @@ "message": "Nic k zobrazení" }, "nothingGeneratedRecently": { - "message": "Nedávno jste nic nevygenerovali" + "message": "Ještě jste nic nevygenerovali" }, "remove": { "message": "Odebrat" @@ -2233,7 +2233,7 @@ "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "pro vytvoření silného jedinečného hesla", + "message": "pro vytvoření silného jedinečného hesla.", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultCustomization": { @@ -2685,7 +2685,7 @@ "message": "Změny v zablokovaných doménách byly uloženy" }, "excludedDomainsSavedSuccess": { - "message": "Vyloučené změny domény byly uloženy" + "message": "Změny vyloučené domény byly uloženy" }, "limitSendViews": { "message": "Omezit zobrazení" @@ -2911,7 +2911,7 @@ "message": "Došlo k chybě při ukládání datumu smzání a vypršení platnosti." }, "hideYourEmail": { - "message": "Skryje Vaši e-mailovou adresu před zobrazením." + "message": "Skrýt Vaši e-mailovou adresu" }, "passwordPrompt": { "message": "Zeptat se znovu na hlavní heslo" @@ -3228,7 +3228,7 @@ "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Vyberte doménu, která je podporována vybranou službou", + "message": "Vyberte doménu, která je podporována vybranou službou.", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Tento požadavek již není platný." }, - "areYouTryingToAccessYourAccount": { - "message": "Pokoušíte se získat přístup k Vašemu účtu?" - }, - "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." }, @@ -4151,7 +4132,7 @@ "message": "Zvolte sbírku" }, "importTargetHint": { - "message": "Pokud chcete obsah importovaného souboru přesunout do složky $DESTINATION$, vyberte tuto volbu", + "message": "Pokud chcete obsah importovaného souboru přesunout do: \"$DESTINATION$\", vyberte tuto volbu", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -4426,7 +4407,7 @@ "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Změňte nastavení automatického vyplňování a správy hesel.", + "message": "Změna nastavení automatického vyplňování a nastavení správy hesel.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { @@ -4434,7 +4415,7 @@ "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Změňte nastavení automatického vyplňování a správy hesel.", + "message": "Změna nastavení automatického vyplňování a nastavení správy hesel.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { @@ -4964,13 +4945,13 @@ "message": "Popis pole" }, "textHelpText": { - "message": "Použijte textová pole pro data jako bezpečnostní otázky" + "message": "Použijte textová pole pro data (jako např. bezpečnostní otázky)." }, "hiddenHelpText": { - "message": "Použijte skrytá pole pro citlivá data, jako je heslo" + "message": "Použijte skrytá pole pro citlivá data, jako je heslo." }, "checkBoxHelpText": { - "message": "Použijte zaškrtávací políčka, pokud chcete automaticky vyplnit zaškrtávací políčko formuláře (např. pro zapamatování e-mailu)" + "message": "Použijte zaškrtávací políčka, pokud chcete automaticky zvolit zaškrtávací políčko formuláře (např. pro zapamatování e-mailu)." }, "linkedHelpText": { "message": "Použijte propojené pole, pokud máte problémy s automatickým vyplňováním na konkrétní webové stránce." @@ -5140,7 +5121,7 @@ "message": "Zobrazit akce rychlé kopie v trezoru" }, "systemDefault": { - "message": "Systémový výchozí" + "message": "Výchozí systémový" }, "enterprisePolicyRequirementsApplied": { "message": "Na toto nastavení byly uplatněny požadavky podnikových zásad" @@ -5197,7 +5178,7 @@ "message": "Položky, které smažete, se zde zobrazí a budou trvale smazány po 30 dnech." }, "trashWarning": { - "message": "Položky, které byly v koši déle než 30 dní, budou automaticky smazány." + "message": "Položky, které byly v koši déle než 30 dnů, budou automaticky smazány." }, "restore": { "message": "Obnovit" @@ -5413,10 +5394,10 @@ "message": "Šířka rozšíření" }, "wide": { - "message": "Šířka" + "message": "Široké" }, "extraWide": { - "message": "Extra široký" + "message": "Extra široké" }, "sshKeyWrongPassword": { "message": "Zadané heslo není správné." @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly není ve Vašem prohlížeči podporováno nebo není povoleno. WebAssembly je vyžadováno pro použití aplikace Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Zobrazit více" + }, + "showLess": { + "message": "Zobrazit méně" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 307373da9aa..8307b994fe0 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 4b6da81a994..0ff87e6e14f 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 9ef82a6d5ae..ffaf490fbf8 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Diese Anfrage ist nicht mehr gültig." }, - "areYouTryingToAccessYourAccount": { - "message": "Versuchst du auf dein Konto zuzugreifen?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly wird von deinem Browser nicht unterstützt oder ist nicht aktiviert. WebAssembly wird benötigt, um die Bitwarden-App nutzen zu können.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Mehr anzeigen" + }, + "showLess": { + "message": "Weniger anzeigen" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index fa4f3ac0f3c..3d46b63ec45 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -84,7 +84,7 @@ "message": "Υπόδειξη κύριου κωδικού πρόσβασης (προαιρετικό)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Βαθμολογία ισχύς κωδικού πρόσβασης $SCORE$", "placeholders": { "score": { "content": "$1", @@ -468,13 +468,13 @@ "message": "Ο κωδικός δημιουργήθηκε" }, "passphraseGenerated": { - "message": "Passphrase generated" + "message": "Το συνθηματικό δημιουργήθηκε" }, "usernameGenerated": { - "message": "Username generated" + "message": "Το όνομα χρήστη δημιουργήθηκε" }, "emailGenerated": { - "message": "Email generated" + "message": "Το email δημιουργήθηκε" }, "regeneratePassword": { "message": "Επαναδημιουργία κωδικού πρόσβασης" @@ -548,7 +548,7 @@ "message": "Αναζήτηση στο vault" }, "resetSearch": { - "message": "Reset search" + "message": "Επαναφορά αναζήτησης" }, "edit": { "message": "Επεξεργασία" @@ -659,10 +659,10 @@ "message": "Το πρόγραμμα περιήγησης ιστού δεν υποστηρίζει εύκολη αντιγραφή πρόχειρου. Αντιγράψτε το με το χέρι αντ'αυτού." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Επαλήθευση ταυτότητας" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "Δεν αναγνωρίζουμε αυτή τη συσκευή. Εισάγετε τον κωδικό που στάλθηκε στο email σας για να επαληθεύσετε την ταυτότητά σας." }, "continueLoggingIn": { "message": "Continue logging in" @@ -875,22 +875,22 @@ "message": "Σύνδεση στο Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Εισάγετε τον κωδικό που στάλθηκε στο email σας" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Εισαγάγετε τον κωδικό μιας χρήσης από την εφαρμογή αυθεντικοποίησης" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "Πιέστε το YubiKey σας για ταυτοποίηση" }, "duoTwoFactorRequiredPageSubtitle": { - "message": "Duo two-step login is required for your account. Follow the steps below to finish logging in." + "message": "Απαιτείται σύνδεση Duo δύο βημάτων για το λογαριασμό σας. Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση." }, "followTheStepsBelowToFinishLoggingIn": { - "message": "Follow the steps below to finish logging in." + "message": "Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση με το κλειδί ασφαλείας σας." }, "restartRegistration": { "message": "Επανεκκίνηση εγγραφής" @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index a58bcba21a0..fa249483b7d 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4131,15 +4131,11 @@ "selectImportCollection": { "message": "Select a collection" }, - "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", - "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", - "placeholders": { - "destination": { - "content": "$1", - "example": "folder or collection" - } - } + "importTargetHintCollection": { + "message": "Select this option if you want the imported file contents moved to a collection" + }, + "importTargetHintFolder": { + "message": "Select this option if you want the imported file contents moved to a folder" }, "importUnassignedItemsError": { "message": "File contains unassigned items." @@ -5591,5 +5587,9 @@ }, "showLess": { "message": "Show less" + }, + "moreBreadcrumbs": { + "message": "More breadcrumbs", + "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index a70fbd85123..8acf75e2ee5 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 39de06249fc..281caeac0d5 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 3b681054abc..0e1185148df 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -197,7 +197,7 @@ "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": { - "message": "Autorrellenar" + "message": "Autocompletar" }, "autoFillLogin": { "message": "Autocompletar inicio de sesión" @@ -548,7 +548,7 @@ "message": "Buscar en caja fuerte" }, "resetSearch": { - "message": "Reset search" + "message": "Restablecer búsqueda" }, "edit": { "message": "Editar" @@ -1456,7 +1456,7 @@ "message": "Copiar TOTP automáticamente" }, "disableAutoTotpCopyDesc": { - "message": "Si tu entrada tiene una clave de autenticación adjunta, el código de verificación TOTP es copiado automáticamente al portapapeles cuando autorellenas una entrada." + "message": "Si tu entrada tiene una clave de autenticación, copiar el código de verificación TOTP a tu portapapeles cuando autocompletes el inicio de sesión." }, "enableAutoBiometricsPrompt": { "message": "Pedir datos biométricos al ejecutar" @@ -1663,25 +1663,25 @@ "message": "Autocompletar al cargar la página" }, "enableAutoFillOnPageLoad": { - "message": "Habilitar autorrellenar al cargar la página" + "message": "Autocompletar al cargar la página" }, "enableAutoFillOnPageLoadDesc": { - "message": "Si se detecta un formulario, realizar automáticamente un autorellenado cuando la web cargue." + "message": "Si se detecta un formulario de inicio de sesión, autocompletar cuando cargue la página web." }, "experimentalFeature": { - "message": "Los sitios web vulnerados o no confiables pueden explotar el autorelleno al cargar la página." + "message": "Los sitios web vulnerados o no confiables pueden explotar el autocompletado al cargar la página." }, "learnMoreAboutAutofillOnPageLoadLinkText": { "message": "Más información sobre riesgos" }, "learnMoreAboutAutofill": { - "message": "Más información sobre el relleno automático" + "message": "Más información sobre el autocompletado" }, "defaultAutoFillOnPageLoad": { "message": "Configuración de autorrelleno por defecto para elementos de inicio de sesión" }, "defaultAutoFillOnPageLoadDesc": { - "message": "Después de activar el autorelleno en Carga de página, puede activar o desactivar la función para entradas individuales. Esta es la configuración predeterminada para elementos de inicio de sesión que no están configurados por separado." + "message": "Puedes desactivar el autocompletado al cargar la página para elementos individuales de inicio de sesión desde la vista de Edición del elemento." }, "itemAutoFillOnPageLoad": { "message": "Auto-relleno en carga de página (si está habilitado en opciones)" @@ -2281,16 +2281,16 @@ "message": "Confirmación de la acción del tiempo de espera" }, "autoFillAndSave": { - "message": "Autorellenar y guardar" + "message": "Autocompletar y guardar" }, "fillAndSave": { "message": "Rellenar y guardar" }, "autoFillSuccessAndSavedUri": { - "message": "Objeto autorellenado y URI guardada" + "message": "Elemento autocompletado y URI guardada" }, "autoFillSuccess": { - "message": "Objeto autorellenado" + "message": "Elemento autocompletado " }, "insecurePageWarning": { "message": "Atención: Esta es una página HTTP no segura, y cualquier información que envíes puede ser vista y cambiada por otros. Este inicio de sesión fue guardado originalmente en una página segura (HTTPS)." @@ -2528,7 +2528,7 @@ "message": "El autorrelleno y otras funcionalidades relacionadas no se ofrecerán para estos sitios web. Debe actualizar la página para que los cambios surtan efecto." }, "autofillBlockedNoticeV2": { - "message": "Autorelleno está desactivado para este sitio web." + "message": "Autocompletado está bloqueado para este sitio web." }, "autofillBlockedNoticeGuidance": { "message": "Cambia esto en los ajustes" @@ -2709,7 +2709,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": "Detalles del Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -2932,7 +2932,7 @@ "message": "Debes verificar tu correo electrónico para usar esta función. Puedes verificar tu correo electrónico en la caja fuerte web." }, "masterPasswordSuccessfullySet": { - "message": "Master password successfully set" + "message": "Contraseña maestra establecida correctamente" }, "updatedMasterPassword": { "message": "Contraseña maestra actualizada" @@ -3106,7 +3106,7 @@ "message": "Exportando caja fuerte personal" }, "exportingIndividualVaultDescription": { - "message": "Solo se exportarán los elementos individuales de la caja fuerte asociados con $EMAIL$. Los elementos de la bóveda de la organización no se incluirán. Solo se exportará la información de los elementos individuales y no incluirá adjuntos asociados.", + "message": "Solo se exportarán los elementos individuales de la caja fuerte asociados a $EMAIL$. Los elementos de la caja fuerte de la organización no se incluirán. Solo se exportará la información de los elementos de la caja fuerte y no se incluirá adjuntos asociados.", "placeholders": { "email": { "content": "$1", @@ -3483,13 +3483,13 @@ "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." }, "device": { - "message": "Device" + "message": "Dispositivo" }, "loginStatus": { "message": "Login status" }, "masterPasswordChanged": { - "message": "Master password saved" + "message": "Contraseña maestra guardada" }, "exposedMasterPassword": { "message": "Contraseña maestra comprometida" @@ -3525,7 +3525,7 @@ "message": "Las políticas de su organización han activado autocompletar al cargar la página." }, "howToAutofill": { - "message": "Cómo autorellenar" + "message": "Cómo autocompletar" }, "autofillSelectInfoWithCommand": { "message": "Selecciona un elemento de esta pantalla, usa el acceso directo $COMMAND$ o explora otras opciones en ajustes.", @@ -3585,28 +3585,28 @@ "message": "Remember this device to make future logins seamless" }, "manageDevices": { - "message": "Manage devices" + "message": "Gestionar dispositivos" }, "currentSession": { - "message": "Current session" + "message": "Sesión actual" }, "mobile": { - "message": "Mobile", + "message": "Móvil", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "Extensión", "description": "Browser extension/addon" }, "desktop": { - "message": "Desktop", + "message": "Escritorio", "description": "Desktop app" }, "webVault": { - "message": "Web vault" + "message": "Caja fuerte Web" }, "webApp": { - "message": "Web app" + "message": "Aplicación Web" }, "cli": { "message": "CLI" @@ -3616,22 +3616,22 @@ "description": "Software Development Kit" }, "requestPending": { - "message": "Request pending" + "message": "Solicitud pendiente" }, "firstLogin": { - "message": "First login" + "message": "Primer inicio de sesión" }, "trusted": { - "message": "Trusted" + "message": "De confianza" }, "needsApproval": { - "message": "Needs approval" + "message": "Necesita aprobación" }, "devices": { - "message": "Devices" + "message": "Dispositivos" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "Intento de acceso de $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3640,16 +3640,16 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "Confirmar acceso" }, "denyAccess": { - "message": "Deny access" + "message": "Denegar acceso" }, "time": { "message": "Time" }, "deviceType": { - "message": "Device Type" + "message": "Tipo de Dispositivo" }, "loginRequest": { "message": "Login request" @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5583,14 +5564,20 @@ "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Crea fácilmente contraseñas seguras y únicas haciendo clic en el botón Generar contraseña para ayudarte a mantener tus inicios de sesión seguros.", "description": "Aria label for the body content of the generator nudge" }, "noPermissionsViewPage": { "message": "No tienes permisos para ver esta página. Intenta iniciar sesión con otra cuenta." }, "wasmNotSupported": { - "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", + "message": "WebAssembly no está soportado por tu navegador o no está habilitado. WebAssembly es necesario para usar la aplicación de Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 8b61aa70a60..09e77bd361f 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 73bd992dacb..1cd1439e972 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 18afcb775f9..cfae87fd1a5 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly در مرورگر شما پشتیبانی نمی‌شود یا فعال نیست. برای استفاده از برنامه Bitwarden، فعال بودن WebAssembly الزامی است.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 894b50b5273..ffd5caea1d3 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Tämä pyyntö ei ole enää voimassa." }, - "areYouTryingToAccessYourAccount": { - "message": "Yritätkö kirjautua tilillesi?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "Kirjautuminen vahvistettu tunnuksella $EMAIL$ laitteella $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Estit toisen laitteen lähettämän kirjautumispyynnön. Jos kuitenkin tunnistit kirjautumisyrityksen, suorita kirjautuminen uudelleen." - }, "loginRequestHasAlreadyExpired": { "message": "Kirjautumispyyntö on jo vanhentunut." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly ei ole tuettu selaimessasi tai se ei ole käytössä. WebAssembly vaaditaan, jotta voi käyttää Bitwardenia.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index dfc8d65cfd9..1682d21aa7f 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index c35f4ca4bb9..1a4f466a41c 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Cette demande n'est plus valide." }, - "areYouTryingToAccessYourAccount": { - "message": "Essayez-vous d'accéder à votre compte ?" - }, - "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 reconnecter avec l'appareil." - }, "loginRequestHasAlreadyExpired": { "message": "La demande de connexion a déjà expiré." }, @@ -4601,7 +4582,7 @@ } }, "copyFieldCipherName": { - "message": "Copiez $FIELD$, $CIPHERNAME$", + "message": "Copier $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly n'est pas pris en charge sur votre navigateur ou n'est pas activé. WebAssembly est requis pour utiliser l'application Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index c7d47b61209..0698191f53e 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index c13bcad10c6..96dc5215051 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index cc89f9cc2dd..35d7e8b2911 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 97bd79f2950..d38461ab553 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Ovaj zahtjev više nije valjan." }, - "areYouTryingToAccessYourAccount": { - "message": "Pokušavaš li pristupiti svom računu?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly ili nije podržan ili nije omogućen u tvom pregledniku. WebAssembly je potreban za korištenje Bitwarden aplikacije.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Prikaži više" + }, + "showLess": { + "message": "Pokaži manje" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index cfe79ea4b20..a0fc88e047a 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "A kérés a továbbiakban már nem érvényes." }, - "areYouTryingToAccessYourAccount": { - "message": "A fiókhoz próbálunk hozzáférni?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "A WebAssembly nem támogatott a böngészőben vagy nincs engedélyezve. A WebAssembly szükséges a Bitwarden alkalmazás használatához.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Több megjelenítése" + }, + "showLess": { + "message": "Kevesebb megjelenítése" } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 4ce58ee7618..297b2d458e1 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -20,7 +20,7 @@ "message": "Undangan diterima" }, "createAccount": { - "message": "Buat Akun" + "message": "Buat akun" }, "newToBitwarden": { "message": "Baru menggunakan Bitwarden?" @@ -548,7 +548,7 @@ "message": "Cari brankas" }, "resetSearch": { - "message": "Reset search" + "message": "Atur ulang pencarian" }, "edit": { "message": "Edit" @@ -1177,10 +1177,10 @@ "description": "Detailed error message shown when saving login details fails." }, "changePasswordWarning": { - "message": "After changing your password, you will need to log in with your new password. Active sessions on other devices will be logged out within one hour." + "message": "Setelah mengubah kata sandi, Anda perlu masuk lagi dengan kata sandi baru. Sesi aktif di perangkat lain akan keluar dalam satu jam." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Change your master password to complete account recovery." + "message": "Ubah kata sandi utama Anda untuk menyelesaikan pemulihan akun." }, "enableChangedPasswordNotification": { "message": "Tanyakan untuk memperbarui masuk yang sudah ada" @@ -1833,7 +1833,7 @@ "message": "Kode Keamanan" }, "cardNumber": { - "message": "card number" + "message": "nomor kartu" }, "ex": { "message": "mis." @@ -2500,10 +2500,10 @@ "message": "Sebuah kebijakan organisasi telah menghalangi mengimpor benda-benda ke brankas pribadi Anda." }, "restrictCardTypeImport": { - "message": "Cannot import card item types" + "message": "Tidak dapat meingpor jenis item kartu" }, "restrictCardTypeImportDesc": { - "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + "message": "Kebijakan yang diatur 1 atau lebih organisasi tidak mengizinkan Anda mengimpor kartu ke brankas." }, "domainsTitle": { "message": "Domain", @@ -2932,7 +2932,7 @@ "message": "Anda harus memverifikasi email Anda untuk menggunakan fitur ini. Anda dapat memverifikasi email Anda di brankas web." }, "masterPasswordSuccessfullySet": { - "message": "Master password successfully set" + "message": "Kata sandi utama berhasil diatur" }, "updatedMasterPassword": { "message": "Kata Sandi Utama Telah Diperbarui" @@ -3467,7 +3467,7 @@ "message": "Permintaan terkirim" }, "loginRequestApprovedForEmailOnDevice": { - "message": "Login request approved for $EMAIL$ on $DEVICE$", + "message": "Permintaan masuk untuk $EMAIL$ di $DEVICE$ diizinkan", "placeholders": { "email": { "content": "$1", @@ -3480,16 +3480,16 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + "message": "Anda menolak percobaan masuk dari perangkat lain. Jika itu adalah Anda, mohon coba masuk kembali di perangkat tersebut." }, "device": { - "message": "Device" + "message": "Perangkat" }, "loginStatus": { - "message": "Login status" + "message": "Status masuk" }, "masterPasswordChanged": { - "message": "Master password saved" + "message": "Sandi utama disimpan" }, "exposedMasterPassword": { "message": "Kata Sandi Utama yang Terpapar" @@ -3585,17 +3585,17 @@ "message": "Ingat perangkat ini untuk membuat login berikutnya lebih lancar" }, "manageDevices": { - "message": "Manage devices" + "message": "Kelola perangkat" }, "currentSession": { - "message": "Current session" + "message": "Sesi saat ini" }, "mobile": { - "message": "Mobile", + "message": "Seluler", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "Pengaya", "description": "Browser extension/addon" }, "desktop": { @@ -3603,35 +3603,35 @@ "description": "Desktop app" }, "webVault": { - "message": "Web vault" + "message": "Brankas web" }, "webApp": { - "message": "Web app" + "message": "Aplikasi web" }, "cli": { - "message": "CLI" + "message": "Antarmuka Baris Perintah" }, "sdk": { "message": "SDK", "description": "Software Development Kit" }, "requestPending": { - "message": "Request pending" + "message": "Permintaan tertunda" }, "firstLogin": { - "message": "First login" + "message": "Masuk pertama" }, "trusted": { - "message": "Trusted" + "message": "Terpercaya" }, "needsApproval": { - "message": "Needs approval" + "message": "Perlu persetujuan" }, "devices": { - "message": "Devices" + "message": "Perangkat" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "Percobaan akses oleh $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3640,50 +3640,31 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "Izinkan akses" }, "denyAccess": { - "message": "Deny access" + "message": "Tolak akses" }, "time": { - "message": "Time" + "message": "Waktu" }, "deviceType": { - "message": "Device Type" + "message": "Jenis Perangkat" }, "loginRequest": { - "message": "Login request" + "message": "Permintaan masuk" }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." - }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." + "message": "Permintaan ini tidak lagi valid." }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "Permintaan masuk kedaluwarsa." }, "justNow": { - "message": "Just now" + "message": "Baru saja" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "Diminta $MINUTES$ menit yang lalu", "placeholders": { "minutes": { "content": "$1", @@ -3713,10 +3694,10 @@ "message": "Minta persetujuan admin" }, "unableToCompleteLogin": { - "message": "Unable to complete login" + "message": "Gagal menyelesaikan proses masuk" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { - "message": "You need to log in on a trusted device or ask your administrator to assign you a password." + "message": "Anda harus masuk di perangkat terpercaya atau meminta administrator untuk membuat kata sandi Anda." }, "ssoIdentifierRequired": { "message": "Pengenal SSO organisasi diperlukan." @@ -4398,23 +4379,23 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "message": "Bitwarden mengenali saran isi otomatis menggunakan deteksi kecocokan URI.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { - "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "message": "\"Ekspresi regular\" adalah opsi lanjutan dengan risiko lebih besar untuk menyingkapkan identitas anda.", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { - "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "message": "\"Bermula dengan\" adalah opsi lanjutan dengan risiko lebih besar untuk menyingkapkan identitas anda.", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "More about match detection", + "message": "Ketahui lebih tentang deteksi kecocokan", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { - "message": "Advanced options", + "message": "Opsi lanjutan", "description": "Advanced option placeholder for uri option component" }, "confirmContinueToBrowserSettingsTitle": { @@ -4601,7 +4582,7 @@ } }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Salin $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -5245,7 +5226,7 @@ "message": "PIN untuk membuka telah diatur" }, "unlockWithBiometricSet": { - "message": "Unlock with biometrics set" + "message": "Biometrik untuk membuka telah diatur" }, "authenticating": { "message": "Sedang memeriksa keaslian" @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly tidak didukung atau tidak dinyalakan oleh peramban Anda. Web Assembly diperlukan untuk menggunakan aplikasi Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 164d46bcec5..4e1c1d63e50 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -548,7 +548,7 @@ "message": "Cerca nella cassaforte" }, "resetSearch": { - "message": "Reset search" + "message": "Svuota ricerca" }, "edit": { "message": "Modifica" @@ -1177,10 +1177,10 @@ "description": "Detailed error message shown when saving login details fails." }, "changePasswordWarning": { - "message": "After changing your password, you will need to log in with your new password. Active sessions on other devices will be logged out within one hour." + "message": "Dopo aver cambiato la password, dovrai accedere con quella nuova. Le sessioni attive su altri dispositivi saranno disconnesse entro un'ora." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Change your master password to complete account recovery." + "message": "Cambia la password principale per completare il recupero dell'account." }, "enableChangedPasswordNotification": { "message": "Chiedi di aggiornare il login esistente" @@ -1833,7 +1833,7 @@ "message": "Codice di sicurezza" }, "cardNumber": { - "message": "card number" + "message": "numero carta" }, "ex": { "message": "es." @@ -2932,7 +2932,7 @@ "message": "Devi verificare la tua email per usare questa funzionalità. Puoi verificare la tua email nella cassaforte web." }, "masterPasswordSuccessfullySet": { - "message": "Master password successfully set" + "message": "Password principale impostata correttamente" }, "updatedMasterPassword": { "message": "Password principale aggiornata" @@ -3467,7 +3467,7 @@ "message": "Richiesta inviata" }, "loginRequestApprovedForEmailOnDevice": { - "message": "Login request approved for $EMAIL$ on $DEVICE$", + "message": "Richiesta di accesso approvata per $EMAIL$ su $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3480,16 +3480,16 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + "message": "Hai negato un tentativo di accesso da un altro dispositivo. Se eri davvero tu, prova di nuovo ad accedere con il dispositivo." }, "device": { - "message": "Device" + "message": "Dispositivo" }, "loginStatus": { - "message": "Login status" + "message": "Stato login" }, "masterPasswordChanged": { - "message": "Master password saved" + "message": "Password principale salvata" }, "exposedMasterPassword": { "message": "Password principale violata" @@ -3585,17 +3585,17 @@ "message": "Ricorda questo dispositivo per rendere immediati i futuri accessi" }, "manageDevices": { - "message": "Manage devices" + "message": "Gestisci dispositivi" }, "currentSession": { - "message": "Current session" + "message": "Sessione in corso" }, "mobile": { "message": "Mobile", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "Estensione", "description": "Browser extension/addon" }, "desktop": { @@ -3603,35 +3603,35 @@ "description": "Desktop app" }, "webVault": { - "message": "Web vault" + "message": "Cassaforte Web" }, "webApp": { - "message": "Web app" + "message": "Bitwarden Web" }, "cli": { - "message": "CLI" + "message": "Linea di comando" }, "sdk": { "message": "SDK", "description": "Software Development Kit" }, "requestPending": { - "message": "Request pending" + "message": "Richiesta in attesa" }, "firstLogin": { - "message": "First login" + "message": "Primo accesso" }, "trusted": { - "message": "Trusted" + "message": "Attendibile" }, "needsApproval": { - "message": "Needs approval" + "message": "In attesa di approvazione" }, "devices": { - "message": "Devices" + "message": "Dispositivi" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "Tentativo di accesso da $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3640,50 +3640,31 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "Conferma l'accesso" }, "denyAccess": { - "message": "Deny access" + "message": "Nega l'accesso" }, "time": { - "message": "Time" + "message": "Ora" }, "deviceType": { - "message": "Device Type" + "message": "Tipo di dispositivo" }, "loginRequest": { - "message": "Login request" + "message": "Richiesta di accesso" }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." - }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." + "message": "Questa richiesta non è più valida." }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "La richiesta di accesso è scaduta." }, "justNow": { - "message": "Just now" + "message": "Pochi secondi fa" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "Richiesta: $MINUTES$ minuti fa", "placeholders": { "minutes": { "content": "$1", @@ -3713,10 +3694,10 @@ "message": "Richiedi approvazione dell'amministratore" }, "unableToCompleteLogin": { - "message": "Unable to complete login" + "message": "Impossibile completare l'accesso" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { - "message": "You need to log in on a trusted device or ask your administrator to assign you a password." + "message": "È necessario accedere da un dispositivo attendibile oppure chiedere all'amministratore l'assegnazione di una password." }, "ssoIdentifierRequired": { "message": "Identificatore SSO dell'organizzazione obbligatorio." @@ -4398,23 +4379,23 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "message": "La corrispondenza URL è il metodo predefinito per identificare i suggerimenti di riempimento automatico.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { - "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "message": "'Espressione regolare' è un'opzione avanzata con un maggior rischio di esporre le credenziali.", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { - "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "message": "'Inizia con' è un'opzione con un maggior rischio di esporre le credenziali.", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "More about match detection", + "message": "Ulteriori informazioni sulla corrispondenza degli URL", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { - "message": "Advanced options", + "message": "Opzioni avanzate", "description": "Advanced option placeholder for uri option component" }, "confirmContinueToBrowserSettingsTitle": { @@ -4601,7 +4582,7 @@ } }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Copia $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly non è supportato dal browser o non è abilitato. WebAssembly è richiesto per utilizzare l'app Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Mostra di più" + }, + "showLess": { + "message": "Mostra di meno" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 977d04ca28a..9370a3ae6b1 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index da70c255803..6bbdb6f67f3 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index f4c58318bc1..d076ef01ef2 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 31b285f6780..f4ab488884a 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 89d59488aaf..50725e73064 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -747,7 +747,7 @@ "message": "시스템 잠금 시" }, "onRestart": { - "message": "브라우저 다시 시작 시" + "message": "브라우저 재시작 시" }, "never": { "message": "안함" @@ -1016,7 +1016,7 @@ "description": "This is the folder for uncategorized items" }, "enableAddLoginNotification": { - "message": "로그인을 추가할 건지 물어보기" + "message": "로그인 추가 확인" }, "vaultSaveOptionsTitle": { "message": "보관함 옵션들을 저장하기" @@ -1028,7 +1028,7 @@ "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, "showCardsInVaultViewV2": { - "message": "보관함 보기에서 언제나 카드 자동 완성 제안을 표시" + "message": "보관함 보기에서 자동 완성 제안으로 카드를 항상 표시" }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" @@ -1037,7 +1037,7 @@ "message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열" }, "showIdentitiesInVaultViewV2": { - "message": "보관함 보기에서 언제나 신원의 자동 완성 제안을 표시" + "message": "보관함 보기에서 자동 채우기 제안으로 신원을 항상 표시" }, "showIdentitiesCurrentTab": { "message": "탭 페이지에 신원들을 표시" @@ -1049,7 +1049,7 @@ "message": "보관함 보기에서 항목을 클릭하여 자동 완성" }, "clickToAutofill": { - "message": "Click items in autofill suggestion to fill" + "message": "자동 완성 제안에서 항목 클릭으로 채우기" }, "clearClipboard": { "message": "클립보드 비우기", @@ -1183,7 +1183,7 @@ "message": "Change your master password to complete account recovery." }, "enableChangedPasswordNotification": { - "message": "현재 로그인으로 업데이트할 건지 묻기" + "message": "기존 로그인 정보 업데이트 확인" }, "changedPasswordNotificationDesc": { "message": "웹사이트에서 변경 사항이 감지되면 로그인 비밀번호를 업데이트하라는 메시지를 표시합니다." @@ -1213,7 +1213,7 @@ "message": "추가 옵션" }, "enableContextMenuItem": { - "message": "문맥 매뉴 옵션 표시" + "message": "컨텍스트 메뉴 옵션 표시" }, "contextMenuItemDesc": { "message": "우클릭을 사용하여, 비밀번호 생성과 웹사이트 로그인 매칭에 접근하세요" @@ -1264,10 +1264,10 @@ "message": "이 비밀번호는 이 파일을 파일 내보내거나, 가져오는데 사용됩니다." }, "accountRestrictedOptionDescription": { - "message": "내보내기를 당신의 계정의 사용자이름과 마스터비밀번호로부터 파생된 계정 암호화 키를 사용하여 암호화하고, 현재의 Bitwarden 계정으로만 가져오도록 제한합니다." + "message": "당신 계정의 사용자 이름과 마스터 비밀번호를 통한 암호화 키로 내보내기 파일을 암호화 하고, 현재의 Bitwarden 계정으로만 가져올 수 있도록 제한합니다." }, "passwordProtectedOptionDescription": { - "message": "파일에 비밀번호를 설정하여 내보내기를 암호화하고, 어느 Bitwarden 계정으로든 그 비밀번호로 해독하여 가져오기 합니다." + "message": "파일에 비밀번호를 설정하여 내보내기 파일을 암호화하며, 그 비밀번호만 입력하면 어느 Bitwarden 계정으로든 가져올 수 있습니다." }, "exportTypeHeading": { "message": "내보내기 유형" @@ -1630,13 +1630,13 @@ "message": "양식 필드에 자동 완성 제안 표시" }, "showInlineMenuIdentitiesLabel": { - "message": "신원를 제안으로 표시" + "message": "신원을 제안으로 표시" }, "showInlineMenuCardsLabel": { "message": "카드를 제안으로 표시" }, "showInlineMenuOnIconSelectionLabel": { - "message": "아이콘을 선택할 때 제안을 표시" + "message": "아이콘 선택 시 제안 표시" }, "showInlineMenuOnFormFieldsDescAlt": { "message": "로그인한 모든 계정에 적용" @@ -2196,7 +2196,7 @@ "message": "브라우저 다시 시작 시 마스터 비밀번호로 잠금" }, "lockWithMasterPassOnRestart1": { - "message": "브라우저 다시 시작 시 마스터 비밀번호가 필요합니다" + "message": "브라우저 재시작 시 마스터 비밀번호 요구" }, "selectOneCollection": { "message": "반드시 하나 이상의 컬렉션을 선택해야 합니다." @@ -3115,7 +3115,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "message": "$EMAIL$ 계정과 관련된 개인 보관함 항목과 첨부 파일만 내보내집니다. 조직 보관함 항목은 포함되지 않습니다.", "placeholders": { "email": { "content": "$1", @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -3808,7 +3789,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", + "message": "플랫폼에 구애받지 않고 누구에게나 파일과 데이터를 안전하게 공유하세요. 전송하는 정보는 종단 간 암호화로 보호되어 외부 노출이 최소화됩니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -4398,7 +4379,7 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "message": "Bitwarden에서 자동 채우기 제안을 식별하는 방법은 URI 일치 감지입니다.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 5a4f0d8c419..fa53a42a4b7 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 81447182f26..68d5a0db2e3 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -605,7 +605,7 @@ "message": "Izdzēst vienumu" }, "viewItem": { - "message": "Skatīt vienumu" + "message": "Apskatīt vienumu" }, "launch": { "message": "Palaist" @@ -1180,7 +1180,7 @@ "message": "Pēc savas paroles nomainīšanas būs nepieciešams pieteikties ar jauno paroli. Spēkā esošajās sesijās citās ierīcēs stundas laikā notiks atteikšanās." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Jānomaina sava galvenā'parole, lai pabeigtu konta atkopi." + "message": "Jānomaina sava galvenā parole, lai pabeigtu konta atkopi." }, "enableChangedPasswordNotification": { "message": "Vaicāt atjaunināt esošu pieteikšanās vienumu" @@ -1490,7 +1490,7 @@ "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Izmantot savu atkopes kodu" + "message": "Izmanto savu atkopes kodu" }, "insertU2f": { "message": "Ievieto savu drošības atslēgu datora USB ligzdā! Ja tai ir poga, pieskaries tai!" @@ -1523,10 +1523,10 @@ "message": "Atlasīt divpakāpju pieteikšanās veidu" }, "recoveryCodeDesc": { - "message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopšanas kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!" + "message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopes kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!" }, "recoveryCodeTitle": { - "message": "Atgūšanas kods" + "message": "Atkopes kods" }, "authenticatorAppTitle": { "message": "Autentificētāja lietotne" @@ -3437,7 +3437,7 @@ "message": "Atkārtoti nosūtīt paziņojumu" }, "viewAllLogInOptions": { - "message": "Skatīt visas pieteikšanās iespējas" + "message": "Apskatīt visas pieteikšanās iespējas" }, "notificationSentDevice": { "message": "Uz ierīci ir nosūtīts paziņojums." @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Šis pieprasījums vairs nav derīgs." }, - "areYouTryingToAccessYourAccount": { - "message": "Vai mēģini piekļūt savam kontam?" - }, - "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." }, @@ -3747,7 +3728,7 @@ "description": "European Union" }, "accessDenied": { - "message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai skatītu šo lapu." + "message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai apskatītu šo lapu." }, "general": { "message": "Vispārīgi" @@ -4406,7 +4387,7 @@ "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { - "message": "\"Sākas ar' ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.", + "message": "“Sākas ar” ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { @@ -4430,7 +4411,7 @@ "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "Paplašinājuma īsinājumtaustiņus skatīt un iestatīt var pārlūka iestatījumos.", + "message": "Paplašinājuma īsinājumtaustiņus apskatīt un iestatīt var pārlūka iestatījumos.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { @@ -4438,7 +4419,7 @@ "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "Paplašinājuma īsinājumtaustiņus skatīt un iestatīt var pārlūka iestatījumos.", + "message": "Paplašinājuma īsinājumtaustiņus apskatīt un iestatīt var pārlūka iestatījumos.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { @@ -4553,7 +4534,7 @@ } }, "viewItemTitle": { - "message": "Skatīt vienumu - $ITEMNAME$", + "message": "Apskatīt vienumu - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4648,7 +4629,7 @@ "message": "Kļūda mērķa mapes piešķiršanā." }, "viewItemsIn": { - "message": "Skatīt $NAME$ vienumus", + "message": "Apskatīt $NAME$ vienumus", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly šajā pārlūkā netiek atbalstīts vai nav iespējots. WebAssebly ir nepieciešams, lai izmantotu Bitwarden lietotni.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Rādīt vairāk" + }, + "showLess": { + "message": "Rādīt mazāk" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 11709a4611b..ca1bbc69297 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index e7d06e4d5f9..70059b1552e 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index f4c58318bc1..d076ef01ef2 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 7cbe8747d3b..9e617957dc8 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Denne forespørselen er ikke lenger gyldig." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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": "Innloggingsforespørselen har allerede utløpt." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index f4c58318bc1..d076ef01ef2 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 0c2bd31935f..4b38d8491aa 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3073,13 +3073,13 @@ "message": "Geen unieke id gevonden." }, "removeMasterPasswordForOrganizationUserKeyConnector": { - "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + "message": "Voor leden van de volgende organisatie is een hoofdwachtwoord niet langer nodig. Bevestig het domein hieronder met de beheerder van je organisatie." }, "organizationName": { - "message": "Organization name" + "message": "Organisatienaam" }, "keyConnectorDomain": { - "message": "Key Connector domain" + "message": "Key Connector domein" }, "leaveOrganization": { "message": "Organisatie verlaten" @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Dit verzoek is niet langer geldig." }, - "areYouTryingToAccessYourAccount": { - "message": "Probeer je toegang te krijgen tot je account?" - }, - "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." }, @@ -4772,7 +4753,7 @@ "message": "Download op Google Play" }, "downloadOnTheAppStore": { - "message": "Download on the App Store" + "message": "Te downloaden in de App Store" }, "permanentlyDeleteAttachmentConfirmation": { "message": "Weet je zeker dat je deze bijlage definitief wilt verwijderen?" @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly wordt niet ondersteund in je browser of is niet ingeschakeld. WebAssembly is vereist om de Bitwarden-app te gebruiken.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Meer weergeven" + }, + "showLess": { + "message": "Minder weergeven" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index f4c58318bc1..d076ef01ef2 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index f4c58318bc1..d076ef01ef2 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 04112b08219..97875409529 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -1153,7 +1153,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Dziękujemy za dbanie o bezpieczeństwo $ORGANIZATION$. Pozostało $TASK_COUNT$ haseł do zaktualizowania.", + "message": "Dziękujemy za zwiększenie bezpieczeństwa organizacji $ORGANIZATION$. Zaktualizuj hasła dla jeszcze $TASK_COUNT$ danych logowania.", "placeholders": { "organization": { "content": "$1" @@ -1468,7 +1468,7 @@ "message": "Konto premium jest wymagane, aby skorzystać z tej funkcji." }, "authenticationTimeout": { - "message": "Limit czasu uwierzytelniania" + "message": "Przekroczono limit czasu uwierzytelniania" }, "authenticationSessionTimedOut": { "message": "Upłynął limit czasu uwierzytelniania. Zaloguj się ponownie." @@ -1523,7 +1523,7 @@ "message": "Wybierz metodę logowania dwustopniowego" }, "recoveryCodeDesc": { - "message": "Utraciłeś dostęp do wszystkich swoich mechanizmów dwustopniowego logowania? Użyj kodów odzyskiwania, aby wyłączyć dwustopniowe logowanie na Twoim koncie." + "message": "Nie masz dostępu do logowania dwustopniowego? Użyj kodu odzyskiwania, aby je wyłączyć." }, "recoveryCodeTitle": { "message": "Kod odzyskiwania" @@ -1681,7 +1681,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": "Możesz wyłączyć autouzupełnianie po załadowaniu strony dla poszczególnych elementów w opcjach konkretnych elementów." }, "itemAutoFillOnPageLoad": { "message": "Automatycznie uzupełniaj po załadowaniu strony (jeśli włączono w opcjach)" @@ -1773,7 +1773,7 @@ "message": "Pokaż licznik na ikonie" }, "badgeCounterDesc": { - "message": "Wskaż, ile masz danych logowania do bieżącej strony internetowej." + "message": "Pokazuje liczbę danych logowania dla obecnej strony internetowej." }, "cardholderName": { "message": "Właściciel karty" @@ -2178,7 +2178,7 @@ "message": "Kod PIN jest nieprawidłowy." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Zbyt wiele nieprawidłowych prób wpisywania PIN. Wylogowywanie." + "message": "Zbyt wiele nieprawidłowych prób wpisywania kodu PIN. Trwa wylogowanie" }, "unlockWithBiometrics": { "message": "Odblokuj biometrią" @@ -2476,13 +2476,13 @@ "message": "Logowanie biometrią nie powiodło się" }, "biometricsFailedDesc": { - "message": "Dane biometryczne nie mogę być użyte, rozważ użycie hasła głównego lub wylogowanie. Jeśli się to powtarza, skontaktuj się z pomocą techniczną Bitwarden." + "message": "Weryfikacja biometryczna nie powiodła się. Zaloguj się za pomocą hasła głównego. Jeśli problem się powtarza, skontaktuj się z pomocą techniczną Bitwarden." }, "nativeMessaginPermissionErrorTitle": { "message": "Uprawnienie nie zostało przyznane" }, "nativeMessaginPermissionErrorDesc": { - "message": "Bez uprawnienia do komunikowania się z aplikacją desktopową Bitwarden nie możemy dostarczyć obsługi danych biometrycznych w rozszerzeniu przeglądarki. Spróbuj ponownie." + "message": "Odblokowanie biometrią jest dostępne dopiero po połączeniu rozszerzenia przeglądarki z aplikacją desktopową Bitwarden. Spróbuj ponownie." }, "nativeMessaginPermissionSidebarTitle": { "message": "Wystąpił błąd żądania uprawnienia" @@ -2503,7 +2503,7 @@ "message": "Nie można zaimportować karty" }, "restrictCardTypeImportDesc": { - "message": "Polityka ustawiona przez 1 lub więcej organizacji uniemożliwia importowanie kart do sejfów." + "message": "Zasada ustawiona przez co najmniej 1 organizację uniemożliwia importowanie kart do sejfów." }, "domainsTitle": { "message": "Domeny", @@ -2610,7 +2610,7 @@ "message": "Sprawdź i zmień 1 zagrożone hasło" }, "reviewAndChangeAtRiskPasswordsPlural": { - "message": "Przejrzyj i zmień $COUNT$ zagrożonych haseł ", + "message": "Sprawdź i zmień zagrożone hasła ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -2631,14 +2631,14 @@ "message": "Sprawdź zagrożone hasła" }, "reviewAtRiskLoginsSlideDesc": { - "message": "Twoje hasła organizacji są zagrożone, ponieważ są słabe, ponownie używane i/lub narażone.", + "message": "Hasła w Twojej organizacji są zagrożone, ponieważ są słabe, identyczne lub ujawnione.", "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustracja listy danych logowania, które są zagrożone." }, "generatePasswordSlideDesc": { - "message": "Szybko wygeneruj silne, unikalne hasło z menu autouzupełniania Bitwarden na stronie narażonej na ryzyko.", + "message": "Wygeneruj silne i unikalne hasło dla zagrożonej strony internetowej za pomocą autouzupełniania Bitwarden.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { @@ -3115,7 +3115,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Tylko poszczególne elementy sejfu łącznie z załącznikami powiązanymi z $EMAIL$ zostaną wyeksportowane. Elementy sejfu organizacji nie będą dołączone", + "message": "Tylko osobisty sejf $EMAIL$ zostanie wyeksportowany. Elementy organizacji nie zostaną uwzględnione.", "placeholders": { "email": { "content": "$1", @@ -3127,7 +3127,7 @@ "message": "Eksportowanie sejfu organizacji" }, "exportingOrganizationVaultDesc": { - "message": "Tylko sejf organizacji powiązany z $ORGANIZATION$ zostanie wyeksportowany. Pozycje w poszczególnych sejfach lub innych organizacji nie będą uwzględnione.", + "message": "Tylko sejf organizacji $ORGANIZATION$ zostanie wyeksportowany. Elementy innych sejfów nie zostaną uwzględnione.", "placeholders": { "organization": { "content": "$1", @@ -3348,7 +3348,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Nieznana usługa przekierowania: '$SERVICENAME$'.", + "message": "Usługa „$SERVICENAME$” jest nieznana.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3380,7 +3380,7 @@ "message": "Nie można uzyskać dostępu do elementów w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, "loggingInTo": { - "message": "Logowanie na $DOMAIN$", + "message": "Serwer: $DOMAIN$", "placeholders": { "domain": { "content": "$1", @@ -3522,10 +3522,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Twoja organizacja włączyła autouzupełnianie podczas wczytywania strony." + "message": "Twoja organizacja włączyła autouzupełnianie po załadowaniu strony." }, "howToAutofill": { - "message": "Jak autouzupełniać" + "message": "Jak uzupełniać dane" }, "autofillSelectInfoWithCommand": { "message": "Wybierz element, użyj skrótu $COMMAND$ lub zobacz inne opcje w ustawieniach.", @@ -3616,7 +3616,7 @@ "description": "Software Development Kit" }, "requestPending": { - "message": "Zapytanie oczekuje" + "message": "Oczekująca prośba" }, "firstLogin": { "message": "Pierwsze logowanie" @@ -3652,30 +3652,11 @@ "message": "Rodzaj urządzenia" }, "loginRequest": { - "message": "Żądanie logowania" + "message": "Prośba logowania" }, "thisRequestIsNoLongerValid": { "message": "Prośba nie jest już ważna." }, - "areYouTryingToAccessYourAccount": { - "message": "Czy próbujesz uzyskać dostęp do swojego konta?" - }, - "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." }, @@ -3713,7 +3694,7 @@ "message": "Poproś administratora o potwierdzenie" }, "unableToCompleteLogin": { - "message": "Nie można ukończyć logowania" + "message": "Logowanie nie powiodło się" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { "message": "Musisz zalogować się na zaufanym urządzeniu lub poprosić administratora o przypisanie hasła." @@ -3747,7 +3728,7 @@ "description": "European Union" }, "accessDenied": { - "message": "Odmowa dostępu. Nie masz uprawnień do przeglądania tej strony." + "message": "Odmowa dostępu. Nie masz uprawnień do wyświetlenia tej strony." }, "general": { "message": "Ogólne" @@ -3777,7 +3758,7 @@ "message": "Nie znaleziono aktywnego adresu e-mail. Trwa wylogowanie." }, "deviceTrusted": { - "message": "Zaufano urządzeniu" + "message": "Urządzenie zostało zaufane" }, "trustOrganization": { "message": "Zaufaj organizacji" @@ -3935,7 +3916,7 @@ "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Automatyczne wypełnianie przy wczytywaniu strony zostało ustawione, aby używać ustawień domyślnych.", + "message": "Autouzupełnianie po załadowaniu strony zostało ustawione do domyślnych ustawień.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { @@ -4046,7 +4027,7 @@ "message": "Błąd importowania" }, "importErrorDesc": { - "message": "Wystąpił problem z danymi, które chcesz zaimportować. Rozwiąż poniższe problemy w Twoim pliku i spróbuj ponownie." + "message": "Wystąpił problem podczas importowania danych. Usuń poniższe błędy w pliku źródłowym i spróbuj ponownie." }, "resolveTheErrorsBelowAndTryAgain": { "message": "Rozwiąż poniższe błędy i spróbuj ponownie." @@ -4082,7 +4063,7 @@ "message": "Oczekiwanie na potwierdzenie" }, "couldNotCompleteBiometrics": { - "message": "Nie można ukończyć z użyciem biometrii." + "message": "Logowanie biometrią nie powiodło się" }, "needADifferentMethod": { "message": "Potrzebujesz innej metody?" @@ -4303,7 +4284,7 @@ "message": "Wpisz jednorazowy kod z aplikacji uwierzytelniającej" }, "lastPassOOBDesc": { - "message": "Zatwierdź żądanie logowania w aplikacji uwierzytelniającej lub wprowadź jednorazowe hasło." + "message": "Potwierdź logowanie w aplikacji uwierzytelniającej lub wpisz jednorazowy kod." }, "passcode": { "message": "Kod" @@ -4373,7 +4354,7 @@ "message": "serwer" }, "hostedAt": { - "message": "hostowany w" + "message": "serwer" }, "useDeviceOrHardwareKey": { "message": "Użyj urządzenia lub klucza sprzętowego" @@ -4398,7 +4379,7 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "Wykrywanie URI polega na tym, jak Bitwarden identyfikuje sugestie autouzupełniania.", + "message": "Wykrywanie dopasowania to sposób, w jaki Bitwarden identyfikuje sugestie autouzupełniania.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { @@ -5044,7 +5025,7 @@ "message": "Element zostanie przeniesiony do organizacji. Nie będziesz już właścicielem elementu." }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ elementów zostanie trwale przeniesionych do wybranej organizacji. Nie będziesz już posiadać tych elementów.", + "message": "Nie będziesz już właścicielem $PERSONAL_ITEMS_COUNT$ elementów przeniesionych do organizacji.", "placeholders": { "personal_items_count": { "content": "$1", @@ -5062,7 +5043,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ elementy zostaną trwale przeniesione do $ORG$. Nie będziesz już posiadać tych elementów.", + "message": "Nie będziesz już właścicielem $PERSONAL_ITEMS_COUNT$ elementów przeniesionych do organizacji $ORG$.", "placeholders": { "personal_items_count": { "content": "$1", @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly nie jest obsługiwany w przeglądarce lub jest wyłączony. WebAssembly jest wymagany do korzystania z aplikacji Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Pokaż więcej" + }, + "showLess": { + "message": "Pokaż mniej" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 46fbb9beca3..ac5afdc9fce 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1935,7 +1935,7 @@ "message": "Chave SSH" }, "typeNote": { - "message": "Note" + "message": "Nota" }, "newItemHeader": { "message": "Nova $TYPE$", @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -3804,11 +3785,11 @@ "message": "Trust user" }, "sendsTitleNoItems": { - "message": "Send sensitive information safely", + "message": "Envie informações sensíveis com segurança", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", + "message": "Compartilhe arquivos e dados com segurança, com qualquer pessoa e em qualquer plataforma. Suas informações ficarão sempre criptografadas de ponta a ponta, garantindo exposição mínima.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -4748,25 +4729,25 @@ } }, "downloadBitwarden": { - "message": "Download Bitwarden" + "message": "Baixar o Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "Baixar o Bitwarden em todos os dispositivos" }, "getTheMobileApp": { - "message": "Get the mobile app" + "message": "Baixar o aplicativo para dispositivos móveis" }, "getTheMobileAppDesc": { - "message": "Access your passwords on the go with the Bitwarden mobile app." + "message": "Acesse as suas senhas em qualquer lugar com o aplicativo móvel Bitwarden." }, "getTheDesktopApp": { "message": "Get the desktop app" }, "getTheDesktopAppDesc": { - "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." + "message": "Acesse o seu cofre sem um navegador e, em seguida, configure o desbloqueio com dados biométricos para facilitar o desbloqueio tanto no aplicativo desktop quanto na extensão do navegador." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "Baixar em bitwarden.com agora" }, "getItOnGooglePlay": { "message": "Get it on Google Play" @@ -5464,7 +5445,7 @@ "message": "Alterar senhas vulneráveis" }, "settingsVaultOptions": { - "message": "Vault options" + "message": "Opções do cofre" }, "emptyVaultDescription": { "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 3cd813847d1..d3ce5ec3e48 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Este pedido já não é válido." }, - "areYouTryingToAccessYourAccount": { - "message": "Está a tentar aceder à sua conta?" - }, - "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." }, @@ -3747,7 +3728,7 @@ "description": "European Union" }, "accessDenied": { - "message": "Acesso negado. Não tem permissão para visualizar esta página." + "message": "Acesso negado. Não tem permissão para ver esta página." }, "general": { "message": "Geral" @@ -4402,7 +4383,7 @@ "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { - "message": "A \"expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.", + "message": "A \"Expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "O WebAssembly não é suportado no seu navegador ou não está ativado. O WebAssembly é necessário para utilizar a app Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Mostrar mais" + }, + "showLess": { + "message": "Mostrar menos" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 13fe8aa9482..741b491da13 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index a5a6809a059..88fa89ac73e 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Этот запрос больше не действителен." }, - "areYouTryingToAccessYourAccount": { - "message": "Вы пытаетесь получить доступ к своему аккаунту?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "Вход подтвержден для $EMAIL$ на $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Вы отклонили попытку авторизации с другого устройства. Если это действительно были вы, попробуйте авторизоваться с этого устройства еще раз." - }, "loginRequestHasAlreadyExpired": { "message": "Запрос на вход истек." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly не поддерживается вашим браузером или не включен. WebAssembly необходим для использования приложения Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Больше" + }, + "showLess": { + "message": "Меньше" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 7bc0ba2694a..dd2df6f309c 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 28a687be339..e12a0c97ac4 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -216,7 +216,7 @@ "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { - "message": "Vygenerovať heslo (skopírované)" + "message": "Vygenerovať heslo (+skopírovať)" }, "copyElementIdentifier": { "message": "Kopírovať názov vlastného poľa" @@ -979,7 +979,7 @@ "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { - "message": "Pridaná položka" + "message": "Položka bola pridaná" }, "editedItem": { "message": "Položka upravená" @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Táto žiadosť už nie je platná." }, - "areYouTryingToAccessYourAccount": { - "message": "Snažíte sa získať prístup k svojmu účtu?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "Potvrdené prihlásenie pre $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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly nie je vo vašom prehliadači podporovaný alebo nie je povolený. Na používanie Bitwardenu sa vyžaduje WebAssembly.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Zobraziť viac" + }, + "showLess": { + "message": "Zobraziť menej" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 2db40266cd7..3af24b85889 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 3ee18612553..12d0c5b015e 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Овај захтев више није важећи." }, - "areYouTryingToAccessYourAccount": { - "message": "Да ли покушавате да приступите вашем налогу?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "Пријава потврђена за $EMAIL$ на $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Одбили сте покушај пријаве са другог уређаја. Ако сте то заиста били ви, покушајте поново да се пријавите помоћу уређаја." - }, "loginRequestHasAlreadyExpired": { "message": "Захтев за пријаву је већ истекао." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly није подржано или није уапљено на вашем прегледачу. WebAssembly је потребно да би се користила апликација Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 49d2765bf6e..1174b6ac077 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Denna begäran är inte längre giltig." }, - "areYouTryingToAccessYourAccount": { - "message": "Försöker du komma åt ditt konto?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "Inloggning bekräftad för $EMAIL$ på $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Du har avvisat ett inloggningsförsök från en annan enhet. Om det verkligen var du, försök logga in med enheten igen." - }, "loginRequestHasAlreadyExpired": { "message": "Inloggningsbegäran har redan gått ut." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly stöds inte av din webbläsare eller är inte aktiverat. WebAssembly krävs för att använda Bitwarden-appen.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Visa mer" + }, + "showLess": { + "message": "Visa mindre" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index f4c58318bc1..d076ef01ef2 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index fd92c71c200..2cea91a424e 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 83265497ddf..22a083959a9 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -84,7 +84,7 @@ "message": "Ana parola ipucu (isteğe bağlı)" }, "passwordStrengthScore": { - "message": "Parola Güvenlik Puanı $SCORE$", + "message": "Parola gücü puanı: $SCORE$", "placeholders": { "score": { "content": "$1", @@ -982,7 +982,7 @@ "message": "Kayıt eklendi" }, "editedItem": { - "message": "Kayıt kaydedildi" + "message": "Hesap kaydedildi" }, "deleteItemConfirmation": { "message": "Çöp kutusuna göndermek istediğinizden emin misiniz?" @@ -2500,7 +2500,7 @@ "message": "Bir kuruluş ilkesi, kayıtları kişisel kasanıza içe aktarmayı engelledi." }, "restrictCardTypeImport": { - "message": "Cannot import card item types" + "message": "Kart öge türleri içe aktarılamıyor" }, "restrictCardTypeImportDesc": { "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." @@ -2625,7 +2625,7 @@ "message": "Ayarlarınızı güncelleyin, böylece parolalarınızı hızlıca otomatik doldurabilir ve yeni parolalar oluşturabilirsiniz" }, "reviewAtRiskLogins": { - "message": "Risk altındaki girişleri inceleyin" + "message": "Risk altındaki hesapları inceleyin" }, "reviewAtRiskPasswords": { "message": "Risk altındaki parolaları inceleyin" @@ -3392,7 +3392,7 @@ "message": "Sunucu sürümü" }, "selfHostedServer": { - "message": "şirket içinde barındırılan" + "message": "şirket içi" }, "thirdParty": { "message": "Üçüncü taraf" @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Bu istek artık geçerli değil." }, - "areYouTryingToAccessYourAccount": { - "message": "Hesabınıza erişmeye mi çalışıyorsunuz?" - }, - "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." }, @@ -4009,7 +3990,7 @@ "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "Yeni kasa giriş ögesini yeni bir açılır pencerede ekleyin", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { @@ -4017,7 +3998,7 @@ "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "Yeni kasa kartı ögesini yeni bir açılır pencerede ekleyin", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { @@ -4204,7 +4185,7 @@ "message": "Geçiş anahtarı" }, "accessing": { - "message": "Erişilen konum:" + "message": "Erişilen konum" }, "loggedInExclamation": { "message": "Giriş yapıldı!" @@ -4398,7 +4379,7 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "message": "URl eşleştirme tespiti, Bitwarden'ın otomatik doldurma önerilerini nasıl tanımladığıdır.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { @@ -4410,7 +4391,7 @@ "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "More about match detection", + "message": "Eşleşme tespiti hakkında daha fazla bilgi", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { @@ -5209,22 +5190,22 @@ "message": "Bu kaydı düzenleme yetkisine sahip değilsiniz" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Önce PIN veya parola gerektiğinden biyometrik kilit açılamıyor." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Biyometrik kilit açma şu anda kullanılamıyor." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biyometrik kilit açma, yanlış yapılandırılmış sistem dosyaları nedeniyle kullanılamıyor." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biyometrik kilit açma, yanlış yapılandırılmış sistem dosyaları nedeniyle kullanılamıyor." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Bitwarden masaüstü uygulaması kapalı olduğu için biyometrik kilit açılamıyor." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Biyometrik kilit açma, Bitwarden masaüstü uygulamasında $EMAIL$ etkin olmadığı için kullanılamıyor.", "placeholders": { "email": { "content": "$1", @@ -5233,7 +5214,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Biyometrik kilit açma şu anda bilinmeyen bir nedenden dolayı kullanılamıyor." }, "unlockVault": { "message": "Kasanızın kilidini saniyeler içinde açın" @@ -5560,20 +5541,20 @@ "message": "Developer-friendly SSH access" }, "newSshNudgeBodyOne": { - "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication.", + "message": "Anahtarlarınızı saklayın ve hızlı, şifreli kimlik doğrulama için SSH aracısıyla bağlantı kurun.", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "newSshNudgeBodyTwo": { - "message": "Learn more about SSH agent", + "message": "SSH agent hakkında daha fazla bilgi edinin", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Hızlı bir şekilde parolalar oluşturun" }, "generatorNudgeBodyOne": { - "message": "Easily create strong and unique passwords by clicking on", + "message": "Tıklayarak güçlü ve benzersiz parolaları kolayca oluşturun", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, @@ -5583,7 +5564,7 @@ "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Güvenli oturumlar açmaya yardımcı olmak için Parola Oluştur düğmesine tıklayarak güçlü ve benzersiz parolalar oluşturun.", "description": "Aria label for the body content of the generator nudge" }, "noPermissionsViewPage": { @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "Tarayıcınızda WebAssembly desteklenmiyor veya etkinleştirilmemişt. Bitwarden uygulamasını kullanmak için WebAssembly gereklidir.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Daha fazla göster" + }, + "showLess": { + "message": "Daha az göster" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index a03440efe02..537a9bfd5cf 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Цей запит більше недійсний." }, - "areYouTryingToAccessYourAccount": { - "message": "Ви намагаєтесь отримати доступ до свого облікового запису?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "Підтверджено вхід для $EMAIL$ на $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте ввійти з пристроєм знову." - }, "loginRequestHasAlreadyExpired": { "message": "Термін дії запиту на вхід завершився." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly не підтримується або не ввімкнено у вашому браузері. WebAssembly є обов'язковою вимогою для програми Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Розгорнути" + }, + "showLess": { + "message": "Згорнути" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index f25cc9c51dc..7a596d7c23d 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -1836,7 +1836,7 @@ "message": "số thẻ" }, "ex": { - "message": "Ví dụ:" + "message": "ví dụ." }, "title": { "message": "Tiêu đề" @@ -2458,7 +2458,7 @@ "message": "Nhận dạng sinh trắc học không được hỗ trợ" }, "biometricsNotSupportedDesc": { - "message": "Nhận dạng sinh trắc học trên trình duyệt không được hỗ trợ trên thiết bị này" + "message": "Nhận dạng sinh trắc học trên trình duyệt không được hỗ trợ trên thiết bị này." }, "biometricsNotUnlockedTitle": { "message": "Người dùng đã khoá hoặc đã đăng xuất" @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "Yêu cầu này không còn hiệu lực." }, - "areYouTryingToAccessYourAccount": { - "message": "Bạn đang cố gắng truy cập tài khoản của mình?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "Đã xác nhận đăng nhập cho $EMAIL$ trên $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Bạn đã từ chối một lần đăng nhập từ thiết bị khác. Nếu thực sự là bạn, hãy thử đăng nhập lại bằng thiết bị đó." - }, "loginRequestHasAlreadyExpired": { "message": "Yêu cầu đăng nhập đã hết hạn." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly không được hỗ trợ trên trình duyệt của bạn hoặc chưa được kích hoạt. WebAssembly là yêu cầu bắt buộc để sử dụng ứng dụng Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Xem thêm" + }, + "showLess": { + "message": "Thu gọn" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 2382e6fc971..fff87a949ce 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "此请求已失效。" }, - "areYouTryingToAccessYourAccount": { - "message": "您正在尝试访问您的账户吗?" - }, - "logInConfirmedForEmailOnDevice": { - "message": "已确认 $EMAIL$ 在 $DEVICE$ 上的登录", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "您拒绝了一个来自其他设备的登录尝试。若确实是您本人,请尝试再次发起设备登录。" - }, "loginRequestHasAlreadyExpired": { "message": "登录请求已过期。" }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "您的浏览器不支持 WebAssembly 或 WebAssembly 未启用。使用 Bitwarden App 需要 WebAssembly。", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "显示更多" + }, + "showLess": { + "message": "显示更少" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index d2776cb227d..e8873d74e92 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3657,25 +3657,6 @@ "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, - "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." }, @@ -5592,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.html b/apps/browser/src/auth/popup/account-switching/current-account.component.html index 09342c58756..c16abdadf29 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.html +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.html @@ -2,7 +2,7 @@ diff --git a/apps/browser/src/platform/popup/layout/popup-back.directive.ts b/apps/browser/src/platform/popup/layout/popup-back.directive.ts index ce8ebff5ec5..62d66ab87e5 100644 --- a/apps/browser/src/platform/popup/layout/popup-back.directive.ts +++ b/apps/browser/src/platform/popup/layout/popup-back.directive.ts @@ -1,8 +1,6 @@ -import { Directive, Optional } from "@angular/core"; +import { Directive, inject, model } from "@angular/core"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { BitActionDirective, ButtonLikeAbstraction } from "@bitwarden/components"; +import { BitActionDirective, FunctionReturningAwaitable } from "@bitwarden/components"; import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; @@ -11,15 +9,10 @@ import { PopupRouterCacheService } from "../view-cache/popup-router-cache.servic selector: "[popupBackAction]", }) export class PopupBackBrowserDirective extends BitActionDirective { - constructor( - buttonComponent: ButtonLikeAbstraction, - private router: PopupRouterCacheService, - @Optional() validationService?: ValidationService, - @Optional() logService?: LogService, - ) { - super(buttonComponent, validationService, logService); - - // override `bitAction` input; the parent handles the rest - this.handler.set(() => this.router.back()); - } + private routerCacheService = inject(PopupRouterCacheService); + // Override the required input to make it optional since we set it automatically + override readonly handler = model( + () => this.routerCacheService.back(), + { alias: "popupBackAction" }, + ); } diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html index 014ebc86411..2aac161b9d5 100644 --- a/apps/browser/src/platform/popup/layout/popup-header.component.html +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -19,8 +19,7 @@ bitIconButton="bwi-angle-left" type="button" *ngIf="showBackButton" - [title]="'back' | i18n" - [attr.aria-label]="'back' | i18n" + [label]="'back' | i18n" [bitAction]="backAction" >

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 b63d357cab9..7455921b08b 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -67,14 +67,10 @@ class ExtensionPoppedContainerComponent {} - + - + @@ -102,13 +98,7 @@ class MockAddButtonComponent {} @Component({ selector: "mock-popout-button", template: ` - + `, imports: [IconButtonModule], }) @@ -117,7 +107,7 @@ class MockPopoutButtonComponent {} @Component({ selector: "mock-current-account", template: ` - `, @@ -278,7 +268,13 @@ class MockSettingsPageComponent {} - + `, @@ -343,7 +339,7 @@ export default { generator: "Generator", send: "Send", settings: "Settings", - labelWithNotification: (label: string) => `${label}: New Notification`, + labelWithNotification: (label: string | undefined) => `${label}: New Notification`, }); }, }, @@ -671,17 +667,13 @@ export const WithVirtualScrollChild: Story = { - + diff --git a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts index b177497305b..21f6debc02f 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -1,11 +1,10 @@ -import { mock, MockProxy } from "jest-mock-extended"; +import { mock } from "jest-mock-extended"; import { of } from "rxjs"; import { DomainSettingsService, DefaultDomainSettingsService, } 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 { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -54,14 +53,11 @@ describe("ScriptInjectorService", () => { const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); - let configService: MockProxy; let domainSettingsService: DomainSettingsService; beforeEach(() => { jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock); - configService = mock(); - configService.getFeatureFlag$.mockImplementation(() => of(false)); - domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); domainSettingsService.blockedInteractionsUris$ = of({}); scriptInjectorService = new BrowserScriptInjectorService( diff --git a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts index 34ee4fa0f77..f8b4050a5ce 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts @@ -4,8 +4,8 @@ import { Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncOptions } from "@bitwarden/common/platform/sync/sync.service"; @@ -22,7 +22,7 @@ import { FullSyncFinishedMessage } from "./sync-service.listener"; describe("ForegroundSyncService", () => { const userId = Utils.newGuid() as UserId; - const stateService = mock(); + const tokenService = mock(); const folderService = mock(); const folderApiService = mock(); const messageSender = mock(); @@ -38,7 +38,7 @@ describe("ForegroundSyncService", () => { const stateProvider = new FakeStateProvider(accountService); const sut = new ForegroundSyncService( - stateService, + tokenService, folderService, folderApiService, messageSender, diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts index 2ac75bbec2c..01b1f35239b 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -4,8 +4,8 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CommandDefinition, MessageListener, @@ -31,7 +31,7 @@ export const DO_FULL_SYNC = new CommandDefinition("doFullSync") export class ForegroundSyncService extends CoreSyncService { constructor( - stateService: StateService, + tokenService: TokenService, folderService: InternalFolderService, folderApiService: FolderApiServiceAbstraction, messageSender: MessageSender, @@ -47,7 +47,7 @@ export class ForegroundSyncService extends CoreSyncService { stateProvider: StateProvider, ) { super( - stateService, + tokenService, folderService, folderApiService, messageSender, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index f01809433e3..e2f4561e86c 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -2,11 +2,7 @@ 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 { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/environment-selector/environment-selector.component"; import { activeAuthGuard, authGuard, @@ -400,9 +396,6 @@ const routes: Routes = [ path: "", component: EnvironmentSelectorComponent, outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, }, ], }, @@ -425,9 +418,6 @@ const routes: Routes = [ path: "", component: EnvironmentSelectorComponent, outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, }, ], }, @@ -474,9 +464,6 @@ const routes: Routes = [ path: "", component: EnvironmentSelectorComponent, outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, }, ], }, diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 6a26476de43..fa1e6c237c9 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -28,13 +28,13 @@ import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n"; import { LogoutReason, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.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 { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -102,7 +102,7 @@ export class AppComponent implements OnInit, OnDestroy { private authService: AuthService, private i18nService: I18nService, private router: Router, - private stateService: StateService, + private readonly tokenService: TokenService, private vaultBrowserStateService: VaultBrowserStateService, private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, @@ -321,7 +321,7 @@ export class AppComponent implements OnInit, OnDestroy { } private async clearComponentStates() { - if (!(await this.stateService.getIsAuthenticated())) { + if (!(await firstValueFrom(this.tokenService.hasAccessToken$(this.activeUserId)))) { return; } diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 5150c51d765..2aacd3d3632 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -9,7 +9,6 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; @@ -96,7 +95,6 @@ import "../platform/popup/locales"; TabsV2Component, UserVerificationComponent, RemovePasswordComponent, - EnvironmentSelectorComponent, ], exports: [], providers: [CurrencyPipe, DatePipe], diff --git a/apps/browser/src/popup/scss/tailwind.css b/apps/browser/src/popup/scss/tailwind.css index b49fe912861..54139990356 100644 --- a/apps/browser/src/popup/scss/tailwind.css +++ b/apps/browser/src/popup/scss/tailwind.css @@ -1,9 +1,5 @@ @import "../../../../../libs/components/src/tw-theme.css"; -@tailwind base; -@tailwind components; -@tailwind utilities; - @layer components { /** Safari Support */ html.browser_safari .tw-styled-scrollbar::-webkit-scrollbar { diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 9e750ae7341..1930dbd1d4b 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -8,6 +8,7 @@ import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/ 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 { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/browser/browser-popup-utils"; @@ -27,13 +28,14 @@ export class InitService { private themingService: AbstractThemingService, private sdkLoadService: SdkLoadService, private viewCacheService: PopupViewCacheService, + private readonly migrationRunner: MigrationRunner, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { await this.sdkLoadService.loadAndInit(); - await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations + await this.migrationRunner.waitForCompletion(); // Browser background is responsible for migrations await this.i18nService.init(); this.twoFactorService.init(); await this.viewCacheService.init(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index b498c8433f3..f8f6ed919cc 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -47,6 +47,7 @@ import { import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AutofillSettingsService, @@ -62,6 +63,7 @@ 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 { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service"; @@ -79,7 +81,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co 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"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -342,7 +343,7 @@ const safeProviders: SafeProvider[] = [ provide: SyncService, useClass: ForegroundSyncService, deps: [ - StateService, + TokenService, InternalFolderService, FolderApiServiceAbstraction, MessageSender, @@ -361,7 +362,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DomainSettingsService, useClass: DefaultDomainSettingsService, - deps: [StateProvider, ConfigService], + deps: [StateProvider], }), safeProvider({ provide: AbstractStorageService, @@ -627,6 +628,11 @@ const safeProviders: SafeProvider[] = [ useClass: SsoUrlService, deps: [], }), + safeProvider({ + provide: SystemNotificationsService, + useClass: BrowserSystemNotificationService, + deps: [LogService, PlatformUtilsService], + }), safeProvider({ provide: LoginComponentService, useClass: ExtensionLoginComponentService, diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts index 3d93f5d4e04..860b71794ff 100644 --- a/apps/browser/src/popup/tabs-v2.component.ts +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -1,11 +1,9 @@ import { Component } from "@angular/core"; -import { combineLatest, map, Observable, startWith, switchMap } from "rxjs"; +import { map, Observable, startWith, switchMap } from "rxjs"; import { NudgesService } from "@bitwarden/angular/vault"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { Icons } from "@bitwarden/components"; import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component"; @@ -19,12 +17,9 @@ export class TabsV2Component { private hasActiveBadges$ = this.accountService.activeAccount$ .pipe(getUserId) .pipe(switchMap((userId) => this.nudgesService.hasActiveBadges$(userId))); - protected navButtons$: Observable = combineLatest([ - this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), - this.hasActiveBadges$, - ]).pipe( - startWith([false, false]), - map(([onboardingFeatureEnabled, hasBadges]) => { + protected navButtons$: Observable = this.hasActiveBadges$.pipe( + startWith(false), + map((hasBadges) => { return [ { label: "vault", @@ -49,7 +44,7 @@ export class TabsV2Component { page: "/tabs/settings", icon: Icons.SettingsInactive, iconActive: Icons.SettingsActive, - showBerry: onboardingFeatureEnabled && hasBadges, + showBerry: hasBadges, }, ]; }), @@ -57,6 +52,5 @@ export class TabsV2Component { constructor( private nudgesService: NudgesService, private accountService: AccountService, - private readonly configService: ConfigService, ) {} } diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html index 5d313188d8f..c6ea52aff62 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html @@ -26,7 +26,7 @@ slot="end" bitIconButton="bwi-trash" [bitAction]="deleteSend" - appA11yTitle="{{ 'delete' | i18n }}" + label="{{ 'delete' | i18n }}" > diff --git a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html index 839681889a8..9bba3994357 100644 --- a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html +++ b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html @@ -23,12 +23,6 @@ - - - {{ "moreFromBitwarden" | i18n }} - - - diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html index 567d5277454..f4cc27171ad 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html @@ -7,7 +7,7 @@ size="small" appCopyField="username" [cipher]="cipher" - [appA11yTitle]="'copyUsername' | i18n" + [label]="'copyUsername' | i18n" > @@ -18,7 +18,7 @@ size="small" appCopyField="password" [cipher]="cipher" - [appA11yTitle]="'copyPassword' | i18n" + [label]="'copyPassword' | i18n" > @@ -28,7 +28,7 @@ size="small" appCopyField="totp" [cipher]="cipher" - [appA11yTitle]="'copyVerificationCode' | i18n" + [label]="'copyVerificationCode' | i18n" > @@ -40,7 +40,7 @@ type="button" bitIconButton="bwi-clone" size="small" - [appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableLogin.key : cipher.name" + [label]="'copyFieldCipherName' | i18n: singleCopyableLogin.key : cipher.name" [appCopyField]="singleCopyableLogin.field" [cipher]="cipher" > @@ -49,7 +49,7 @@ type="button" bitIconButton="bwi-clone" size="small" - [appA11yTitle]=" + [label]=" hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) " [disabled]="!hasLoginValues" @@ -86,7 +86,7 @@ size="small" appCopyField="cardNumber" [cipher]="cipher" - [appA11yTitle]="'copyNumber' | i18n" + [label]="'copyNumber' | i18n" > @@ -96,7 +96,7 @@ size="small" appCopyField="securityCode" [cipher]="cipher" - [appA11yTitle]="'copySecurityCode' | i18n" + [label]="'copySecurityCode' | i18n" > @@ -107,7 +107,7 @@ type="button" bitIconButton="bwi-clone" size="small" - [appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableCard.key : cipher.name" + [label]="'copyFieldCipherName' | i18n: singleCopyableCard.key : cipher.name" [appCopyField]="singleCopyableCard.field" [cipher]="cipher" showToast @@ -117,7 +117,7 @@ type="button" bitIconButton="bwi-clone" size="small" - [appA11yTitle]=" + [label]=" hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) " [disabled]="!hasCardValues" @@ -142,7 +142,7 @@ type="button" bitIconButton="bwi-clone" size="small" - [appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableIdentity.key : cipher.name" + [label]="'copyFieldCipherName' | i18n: singleCopyableIdentity.key : cipher.name" [appCopyField]="singleCopyableIdentity.field" [cipher]="cipher" showToast @@ -152,7 +152,7 @@ type="button" bitIconButton="bwi-clone" size="small" - [appA11yTitle]=" + [label]=" hasIdentityValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) " [disabled]="!hasIdentityValues" @@ -180,9 +180,7 @@ type="button" bitIconButton="bwi-clone" size="small" - [appA11yTitle]=" - hasSecureNoteValue ? ('copyNoteTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) - " + [label]="hasSecureNoteValue ? ('copyNoteTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)" appCopyField="secureNote" [cipher]="cipher" > @@ -193,9 +191,7 @@ type="button" bitIconButton="bwi-clone" size="small" - [appA11yTitle]=" - hasSshKeyValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) - " + [label]="hasSshKeyValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)" [disabled]="!hasSshKeyValues" [bitMenuTriggerFor]="sshKeyOptions" > 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 962f0c914f5..42e2779679a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -3,8 +3,7 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - [attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name" - [title]="'moreOptionsTitle' | i18n: cipher.name" + [label]="'moreOptionsLabel' | i18n: cipher.name" [disabled]="decryptionFailure" [bitMenuTriggerFor]="moreOptions" > 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 91feaa433a9..1ab162b56fb 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 @@ -8,7 +8,7 @@ bitIconButton="bwi-sliders" [buttonType]="'muted'" [bitDisclosureTriggerFor]="disclosureRef" - [appA11yTitle]="'filterVault' | i18n" + [label]="'filterVault' | i18n" aria-describedby="filters-applied" >

diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html index 8c76db600ae..9b8380a4214 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html @@ -33,7 +33,7 @@ type="button" buttonType="danger" bitIconButton="bwi-trash" - [appA11yTitle]="(cipher.isDeleted ? 'deleteForever' : 'delete') | i18n" + [label]="(cipher.isDeleted ? 'deleteForever' : 'delete') | i18n" > 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 28be3439f24..51116da2865 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 @@ -78,14 +78,14 @@ describe("ViewV2Component", () => { const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const mockCipherService = { - get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }), + cipherViews$: jest.fn().mockImplementation((userId) => of([mockCipher])), getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}), deleteWithServer: jest.fn().mockResolvedValue(undefined), softDeleteWithServer: jest.fn().mockResolvedValue(undefined), - decrypt: jest.fn().mockResolvedValue(mockCipher), }; beforeEach(async () => { + mockCipherService.cipherViews$.mockClear(); mockCipherService.deleteWithServer.mockClear(); mockCipherService.softDeleteWithServer.mockClear(); mockNavigate.mockClear(); @@ -162,7 +162,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444", mockUserId); + expect(mockCipherService.cipherViews$).toHaveBeenCalledWith(mockUserId); expect(component.cipher).toEqual(mockCipher); })); @@ -210,7 +210,7 @@ describe("ViewV2Component", () => { })); it('invokes `doAutofill` when action="AUTOFILL_ID"', fakeAsync(() => { - params$.next({ action: AUTOFILL_ID }); + params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id }); flush(); // Resolve all promises @@ -218,7 +218,7 @@ describe("ViewV2Component", () => { })); it('invokes `copy` when action="copy-username"', fakeAsync(() => { - params$.next({ action: COPY_USERNAME_ID }); + params$.next({ action: COPY_USERNAME_ID, cipherId: mockCipher.id }); flush(); // Resolve all promises @@ -226,7 +226,7 @@ describe("ViewV2Component", () => { })); it('invokes `copy` when action="copy-password"', fakeAsync(() => { - params$.next({ action: COPY_PASSWORD_ID }); + params$.next({ action: COPY_PASSWORD_ID, cipherId: mockCipher.id }); flush(); // Resolve all promises @@ -234,7 +234,7 @@ describe("ViewV2Component", () => { })); it('invokes `copy` when action="copy-totp"', fakeAsync(() => { - params$.next({ action: COPY_VERIFICATION_CODE_ID }); + params$.next({ action: COPY_VERIFICATION_CODE_ID, cipherId: mockCipher.id }); flush(); // Resolve all promises @@ -243,11 +243,13 @@ describe("ViewV2Component", () => { it("does not set the cipher until reprompt is complete", fakeAsync(() => { let promptPromise: (val?: unknown) => void; - mockCipherService.decrypt.mockImplementationOnce(() => - Promise.resolve({ - ...mockCipher, - reprompt: CipherRepromptType.Password, - }), + mockCipherService.cipherViews$.mockImplementationOnce((userId) => + of([ + { + ...mockCipher, + reprompt: CipherRepromptType.Password, + }, + ]), ); doAutofill.mockImplementationOnce(() => { return new Promise((resolve) => { @@ -256,7 +258,7 @@ describe("ViewV2Component", () => { }); }); - params$.next({ action: AUTOFILL_ID }); + params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id }); flush(); // Flush all pending actions @@ -271,11 +273,13 @@ describe("ViewV2Component", () => { it("does not set the cipher at all if doAutofill fails and reprompt is active", fakeAsync(() => { let promptPromise: (val?: unknown) => void; - mockCipherService.decrypt.mockImplementationOnce(() => - Promise.resolve({ - ...mockCipher, - reprompt: CipherRepromptType.Password, - }), + mockCipherService.cipherViews$.mockImplementationOnce((userId) => + of([ + { + ...mockCipher, + reprompt: CipherRepromptType.Password, + }, + ]), ); doAutofill.mockImplementationOnce(() => { return new Promise((resolve) => { @@ -284,7 +288,7 @@ describe("ViewV2Component", () => { }); }); - params$.next({ action: AUTOFILL_ID }); + params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id }); flush(); // Flush all pending actions @@ -301,11 +305,13 @@ describe("ViewV2Component", () => { "does not set cipher when copy fails for %s", fakeAsync((action: string) => { let promptPromise: (val?: unknown) => void; - mockCipherService.decrypt.mockImplementationOnce(() => - Promise.resolve({ - ...mockCipher, - reprompt: CipherRepromptType.Password, - }), + mockCipherService.cipherViews$.mockImplementationOnce((userId) => + of([ + { + ...mockCipher, + reprompt: CipherRepromptType.Password, + }, + ]), ); copy.mockImplementationOnce(() => { return new Promise((resolve) => { @@ -314,7 +320,7 @@ describe("ViewV2Component", () => { }); }); - params$.next({ action }); + params$.next({ action, cipherId: mockCipher.id }); flush(); // Flush all pending actions @@ -336,7 +342,7 @@ describe("ViewV2Component", () => { .spyOn(BrowserApi, "focusTab") .mockImplementation(() => Promise.resolve()); - params$.next({ action: AUTOFILL_ID, senderTabId: 99 }); + params$.next({ action: AUTOFILL_ID, senderTabId: 99, cipherId: mockCipher.id }); flush(); // Resolve all promises 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 69bce9c47d2..fff59e81046 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 @@ -5,7 +5,7 @@ import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, Observable, switchMap, of } from "rxjs"; +import { firstValueFrom, Observable, switchMap, of, map } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -209,8 +209,12 @@ export class ViewV2Component { } async getCipherData(id: string, userId: UserId) { - const cipher = await this.cipherService.get(id, userId); - return await this.cipherService.decrypt(cipher, userId); + return await firstValueFrom( + this.cipherService.cipherViews$(userId).pipe( + filterOutNullish(), + map((ciphers) => ciphers.find((c) => c.id === id)), + ), + ); } async editCipher() { diff --git a/apps/browser/src/vault/popup/guards/intro-carousel.guard.spec.ts b/apps/browser/src/vault/popup/guards/intro-carousel.guard.spec.ts index 4e850294b0b..887ae726309 100644 --- a/apps/browser/src/vault/popup/guards/intro-carousel.guard.spec.ts +++ b/apps/browser/src/vault/popup/guards/intro-carousel.guard.spec.ts @@ -1,29 +1,23 @@ import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; -import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - import { IntroCarouselService } from "../services/intro-carousel.service"; import { IntroCarouselGuard } from "./intro-carousel.guard"; describe("IntroCarouselGuard", () => { - let mockConfigService: MockProxy; const mockIntroCarouselService = { introCarouselState$: of(true), }; const createUrlTree = jest.fn(); beforeEach(() => { - mockConfigService = mock(); createUrlTree.mockClear(); TestBed.configureTestingModule({ providers: [ { provide: Router, useValue: { createUrlTree } }, - { provide: ConfigService, useValue: mockConfigService }, { provide: IntroCarouselService, useValue: mockIntroCarouselService, @@ -32,22 +26,16 @@ describe("IntroCarouselGuard", () => { }); }); - it("should return true if the feature flag is off", async () => { - mockConfigService.getFeatureFlag.mockResolvedValue(false); - const result = await TestBed.runInInjectionContext(async () => await IntroCarouselGuard()); - expect(result).toBe(true); - }); - it("should navigate to intro-carousel route if feature flag is true and dismissed is true", async () => { - mockConfigService.getFeatureFlag.mockResolvedValue(true); + it("should return true when dismissed is true", async () => { const result = await TestBed.runInInjectionContext(async () => await IntroCarouselGuard()); expect(result).toBe(true); }); - it("should navigate to intro-carousel route if feature flag is true and dismissed is false", async () => { + it("should navigate to intro-carousel route when dismissed is false", async () => { TestBed.overrideProvider(IntroCarouselService, { useValue: { introCarouselState$: of(false) }, }); - mockConfigService.getFeatureFlag.mockResolvedValue(true); + await TestBed.runInInjectionContext(async () => await IntroCarouselGuard()); expect(createUrlTree).toHaveBeenCalledWith(["/intro-carousel"]); }); diff --git a/apps/browser/src/vault/popup/guards/intro-carousel.guard.ts b/apps/browser/src/vault/popup/guards/intro-carousel.guard.ts index 4a825a0b2a7..780c6004732 100644 --- a/apps/browser/src/vault/popup/guards/intro-carousel.guard.ts +++ b/apps/browser/src/vault/popup/guards/intro-carousel.guard.ts @@ -2,23 +2,15 @@ import { inject } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - import { IntroCarouselService } from "../services/intro-carousel.service"; export const IntroCarouselGuard = async () => { const router = inject(Router); - const configService = inject(ConfigService); const introCarouselService = inject(IntroCarouselService); - const hasOnboardingNudgesFlag = await configService.getFeatureFlag( - FeatureFlag.PM8851_BrowserOnboardingNudge, - ); - const hasIntroCarouselDismissed = await firstValueFrom(introCarouselService.introCarouselState$); - if (!hasOnboardingNudgesFlag || hasIntroCarouselDismissed) { + if (hasIntroCarouselDismissed) { return true; } diff --git a/apps/browser/src/vault/popup/services/intro-carousel.service.ts b/apps/browser/src/vault/popup/services/intro-carousel.service.ts index 7d2bb7dedb9..2c523c5a93c 100644 --- a/apps/browser/src/vault/popup/services/intro-carousel.service.ts +++ b/apps/browser/src/vault/popup/services/intro-carousel.service.ts @@ -1,8 +1,6 @@ import { Injectable } from "@angular/core"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { map, Observable } from "rxjs"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { GlobalState, KeyDefinition, @@ -28,17 +26,9 @@ export class IntroCarouselService { map((x) => x ?? false), ); - constructor( - private stateProvider: StateProvider, - private configService: ConfigService, - ) {} + constructor(private stateProvider: StateProvider) {} async setIntroCarouselDismissed(): Promise { - const hasVaultNudgeFlag = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), - ); - if (hasVaultNudgeFlag) { - await this.introCarouselState.update(() => true); - } + await this.introCarouselState.update(() => true); } } 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 616c75c3fa8..f271b255c3e 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 @@ -125,6 +125,10 @@ describe("VaultPopupAutofillService", () => { }); it("should only fetch the current tab once when subscribed to multiple times", async () => { + (BrowserApi.getTabFromCurrentWindow as jest.Mock).mockClear(); + + service.refreshCurrentTab(); + const firstTracked = subscribeTo(service.currentAutofillTab$); const secondTracked = subscribeTo(service.currentAutofillTab$); @@ -195,6 +199,7 @@ describe("VaultPopupAutofillService", () => { // Refresh the current tab so the mockedPageDetails$ are used service.refreshCurrentTab(); + (service as any)._currentPageDetails$ = of(mockPageDetails); }); describe("doAutofill()", () => { 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 4e995e093e6..2d30e857573 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 @@ -4,6 +4,7 @@ import { Injectable } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, + debounceTime, firstValueFrom, map, Observable, @@ -164,6 +165,7 @@ export class VaultPopupAutofillService { }), ); }), + debounceTime(50), shareReplay({ refCount: false, bufferSize: 1 }), ); diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.html b/apps/browser/src/vault/popup/settings/folders-v2.component.html index 8cea05f9c17..b36b5affc23 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.html +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.html @@ -25,7 +25,7 @@ slot="end" type="button" (click)="openAddEditFolderDialog(folder)" - [appA11yTitle]="'editFolderWithName' | i18n: folder.name" + [label]="'editFolderWithName' | i18n: folder.name" bitIconButton="bwi-pencil-square" class="tw-self-end" data-testid="edit-folder-button" 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 11ed2674178..d1e70390844 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 @@ -37,8 +37,7 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - [attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name" - [title]="'moreOptionsTitle' | i18n: cipher.name" + [label]="'moreOptionsLabel' | i18n: cipher.name" [bitMenuTriggerFor]="moreOptions" > diff --git a/apps/browser/store/locales/az/copy.resx b/apps/browser/store/locales/az/copy.resx index b8686ad8ffa..65c416c00d3 100644 --- a/apps/browser/store/locales/az/copy.resx +++ b/apps/browser/store/locales/az/copy.resx @@ -127,22 +127,22 @@ PCMag, WIRED, The Verge, CNET, G2 və daha çoxu tərəfindən ən yaxşı parol meneceri olaraq tanındı! RƏQƏMSAL HƏYATINIZI GÜVƏNDƏ SAXLAYIN -Hər hesab üçün unikal, güclü parollar yaradıb saxlayaraq rəqəmsal həyatınızı güvəndə saxlatyın və məlumat pozuntularına qarşı qorunun. Hər şeyi yalnız sizin müraciət edə biləcəyiniz ucdan uca şifrələnmiş parol seyfində saxlayın. +Hər hesab üçün unikal, güclü parollar yaradıb saxlayaraq rəqəmsal həyatınızı güvəndə saxlatyın və məlumat pozuntularına qarşı qorunun. Hər şeyi yalnız sizin erişə biləcəyiniz ucdan uca şifrələnmiş parol seyfində saxlayın. -DATANIZA HƏR YERDƏN, HƏR CİHAZDAN, İSTƏNİLƏN VAXT MÜRACİƏT EDİN +VERİLƏRİNİZƏ HƏR YERDƏN, HƏR CİHAZDAN, İSTƏNİLƏN VAXT ERİŞİN Limitsiz parolları limitsiz cihazlarda məhdudiyyət olmadan asanlıqla idarə edin, saxlayın, güvənlə qoruyun və paylaşın. HƏR KƏS İNTERNETDƏ GÜVƏNDƏ QALMAQ ÜÇÜN ALƏTLƏRƏ SAHİB OLMALIDIR -Bitwarden-i heç bir reklam və ya satış datası olmadan ödənişsiz istifadə edin. Bitwarden hesab edir ki, hər kəs internetdə güvəndə qalmaq bacarığına sahib olmalıdır. Premium planlar qabaqcıl özəlliklərə müraciət təklif edir. +Bitwarden-i heç bir reklam və ya satış veriləri olmadan ödənişsiz istifadə edin. Bitwarden hesab edir ki, hər kəs internetdə güvəndə qalmaq bacarığına sahib olmalıdır. Premium planlar qabaqcıl özəlliklərə erişim təklif edir. BITWARDEN İLƏ KOMANDALARINIZI GÜCLƏNDİRİN -Komanda və Müəssisələr üçün planlar, professional biznes özəllikləri ilə birgə gəlir. Bəzi nümunələrə SSO inteqrasiyası, öz-özünə sahiblik, kataloq inteqrasiyası və SCIM təqdim etmə, qlobal siyasətlər, API müraciəti, olay jurnalları və daha çoxu daxildir. +Komanda və Müəssisələr üçün planlar, professional biznes özəllikləri ilə birgə gəlir. Bəzi nümunələrə SSO inteqrasiyası, öz-özünə sahiblik, kataloq inteqrasiyası və SCIM təqdim etmə, qlobal siyasətlər, API erişimi, event qeydləri və daha çoxu daxildir. İş gücünüzün güvənliyini qorumaq və həssas məlumatları həmkarlarınızla paylaşmaq üçün Bitwarden-i istifadə edin. Bitwarden-i seçmək üçün daha çox səbəb: Dünya səviyyəli şifrələmə -Parollar, qabaqcıl ucdan uca şifrələmə (AES-256 bit, salted hashtag və PBKDF2 SHA-256) ilə qorunur, beləliklə datanız güvəndə və məxfi qalır. +Parollar, qabaqcıl ucdan uca şifrələmə (AES-256 bit, salted hashtag və PBKDF2 SHA-256) ilə qorunur, beləliklə veriləriniz güvəndə və məxfi qalır. 3-cü tərəf auditləri Bitwarden, müntəzəm olaraq tanınmış təhlükəsizlik firmaları ilə hərtərəfli üçüncü tərəf təhlükəsizlik auditləri keçirir. Bu illik auditlərə Bitwarden IP-ləri, serverləri və veb tətbiqləri arasında mənbə kodu qiymətləndirmələri və nüfuz testi daxildir. @@ -151,7 +151,7 @@ Qabaqcıl 2FA Giriş məlumatlarınızı üçüncü tərəf autentifikatoru, e-poçtla göndərilən kodlar və ya avadanlıq güvənlik açarı və ya keçid açarı kimi FIDO2 WebAuthn kimlik məlumatları ilə güvəndə saxlayın. Bitwarden Send -Ucdan-uca şifrələmə güvənliyini qoruyarkən və pozuntunu məhdudlaşdırarkən dataları birbaşa başqalarına göndərin. +Ucdan-uca şifrələmə güvənliyini qoruyarkən və pozuntunu məhdudlaşdırarkən veriləri birbaşa başqalarına göndərin. Daxili Generator Ziyarət etdiyiniz hər sayt üçün uzun, mürəkkəb və fərqli parollar və unikal istifadəçi adları yaradın. Əlavə məxfilik üçün e-poçt ləqəb provayderləri ilə inteqrasiya edin. @@ -160,7 +160,7 @@ Qlobal Tərcümələr Bitwarden tərcümələri 60-dan çox dildə mövcuddur, Crowdin vasitəsilə qlobal icma tərəfindən tərcümə edilmişdir. Çarpaz Platforma Tətbiqləri -İstənilən brauzerdən, mobil cihazdan və ya masaüstü əməliyyat sistemindən və daha çoxundan Bitwarden Seyfinizdəki həssas dataları güvəndə saxlayın və paylaşın. +İstənilən brauzerdən, mobil cihazdan və ya masaüstü əməliyyat sistemindən və daha çoxundan Bitwarden Seyfinizdəki həssas veriləri güvəndə saxlayın və paylaşın. Bitwarden parollardan daha çox qoruyur Bitwarden-nin ucdan-uca şifrələnmiş kimlik məlumatlarını idarəetmə həlləri, təşkilatlara hər şeyi, o cümlədən tərtibatçı sirrlərini və keçid açarı təcrübələrini qorumaq gücü verir. Bitwarden Sirr Meneceri və Bitwarden Passwordless.dev haqqında daha ətraflı öyrənmək üçün Bitwarden.com saytını ziyarət edin! @@ -169,7 +169,7 @@ Bitwarden-nin ucdan-uca şifrələnmiş kimlik məlumatlarını idarəetmə həl Bitwarden evdə və ya işdə olarkən bütün parol, keçid açarı və həssas məlumatlarınızı asanlıqla qoruyur. - Bir neçə cihaz arasında sinxronlaşdıraraq seyfinizə müraciəti genişləndirin + Sinxronlaşdırın və seyfinizə bir neçə cihazdan erişin Bütün girişlərinizi və parollarınızı güvənli bir seyfdən idarə edin @@ -178,7 +178,7 @@ Bitwarden-nin ucdan-uca şifrələnmiş kimlik məlumatlarını idarəetmə həl Giriş kimlik məlumatlarınızı ziyarət etdiyiniz istənilən veb sayta dərhal avtomatik doldurun - Həmçinin sağ klikləmə menyusu ilə seyfinizə asanlıqla müraciət edə bilərsiniz + Həmçinin sağ klikləmə menyusu ilə seyfinizə asanlıqla erişə bilərsiniz Güclü, təsadüfi və güvənli parolların avtomatik yaradılması diff --git a/apps/browser/store/locales/id/copy.resx b/apps/browser/store/locales/id/copy.resx index 2d64b4ca542..2cd61460ccd 100644 --- a/apps/browser/store/locales/id/copy.resx +++ b/apps/browser/store/locales/id/copy.resx @@ -127,45 +127,45 @@ Dikenal sebagai pengelola sandi terbaik oleh PCMag, WIRED, The Verge, CNET, G2, dan lainnya! AMANKAN KEHIDUPAN DIGITAL ANDA -Amankan kehidupan digital Anda dan dapatkan perlindungan dari peretasan data dengan membuat dan menyimpan kata sandi yang unik dan kuat untuk setiap akun. Rawat semuanya dalam brankas kata sandi terenkripsi dari ujung-ke-ujung yang hanya Anda saja yang dapat mengakses. +Amankan kehidupan digital Anda dan dapatkan perlindungan dari peretasan data dengan membuat dan menyimpan kata sandi yang unik dan kuat untuk setiap akun. Rawat semuanya dalam brankas kata sandi terenkripsi dari ujung-ke-ujung yang hanya bisa diakses oleh Anda. -AKSES DATA ANDA, DI MANA SAJA, KAPAN SAJA, DI PERANGKAT APAPUN +AKSES DATA ANDA, DI MANA SAJA, KAPAN SAJA, DI PERANGKAT APA PUN Kelola, simpan, amankan, dan bagikan tanpa batas dengan mudah kata sandi antar perangkat tak terbatas dan tanpa batasan. -SETIAP ORANG SEBAIKNYA MEMILIKI PERALATAN UNTUK TETAP AMAN KETIKA DARING -Gunakan Bitwarden secara gratis tanpa iklan atau menjual data. Bitwarden percaya setiap orang sebaiknya memiliki kemampuan untuk tetap aman ketika daring. Rencana premium menawarkan akses ke fitur-fitur yang lebih lanjut. +SETIAP ORANG HARUS MEMILIKI PERALATAN UNTUK TETAP AMAN KETIKA DARING +Gunakan Bitwarden secara gratis tanpa iklan atau menjual data. Bitwarden percaya setiap orang harus memiliki kemampuan untuk tetap aman ketika daring. Paket premium menawarkan akses ke fitur-fitur yang lebih lanjut. BERDAYAKAN TIM ANDA DENGAN BITWARDEN -Rencana untuk Teams dan Enterprise datang dengan kemampuan bisnis profesional. Beberapa contoh termasuk pemaduan SSO, hosting mandiri, pemaduan direktori dan pembekalan SCIM, kebijakan global, akses API, log kejadian, dan banyak lagi. +Paket untuk Teams dan Enterprise memiliki kemampuan bisnis profesional, termasuk pemaduan SSO, hosting mandiri, pemaduan direktori dan pembekalan SCIM, kebijakan global, akses API, log kejadian, dan banyak lagi. -Gunakan Bitwarden untuk mengamankan tenaga kerja Anda dan membagikan informasi sensitif kepada rekan kerja. +Gunakan Bitwarden untuk mengamankan kerja Anda dan membagikan informasi sensitif kepada rekan kerja. Alasan lebih lanjut untuk memilih Bitwarden: Enkripsi Kelas Dunia -Kata sandi dilindungi dengan enkripsi ujung-ke-ujung yang lebih lanjut (AES-256 bit, tanda pagar bergaram, dan PBKDF2 SHA-256) sehingga data Anda tetap aman dan privat. +Semua kata sandi dilindungi dengan enkripsi ujung-ke-ujung yang lebih lanjut (AES-256 bit, tanda pagar bergaram, dan PBKDF2 SHA-256) sehingga data Anda tetap aman dan privat. Audit Pihak Ketiga Bitwarden secara rutin melakukan audit keamanan yang dilakukan pihak ketiga secara menyeluruh dengan perusahaan keamanan terkemuka. Audit tahunan ini termasuk penilaian sumber kode dan pengujian penembusan antar IP, server, dan aplikasi web Bitwarden. 2FA Terdepan -Amankan login Anda dengan pengotentikasi pihak ketiga, kode yang dikirim ke surel, atau pengenal WebAuthn FIDO2 seperti kunci keamanan perangkat keras atau kunci sandi. +Amankan proses masuk Anda dengan pengotentikasi pihak ketiga, kode yang dikirim ke surel, atau pengenal WebAuthn FIDO2 seperti kunci keamanan perangkat keras atau kunci sandi. Bitwarden Send -Salurkan data ke orang lain secara langsung sembari menjaga keamanan dari ujung-ke-ujung dan membatasi paparan. +Kirim data ke orang lain secara langsung sembari menjaga keamanan dari ujung-ke-ujung dan membatasi singkapan. -Pembuat Bawaan -Buat kata sandi yang panjang, rumit, dan beda serta nama pengguna unik untuk setiap situs yang Anda kunjungi. Padukan dengan nama lain surel untuk privasi lebih lanjut. +Pembuat Sandi Bawaan +Buat kata sandi yang panjang, rumit, dan berbeda serta nama pengguna unik untuk setiap situs yang Anda kunjungi. Campurkan dengan nama lain surel untuk privasi lebih lanjut. Terjemahan Global Terjemahan Bitwarden hadir dalam lebih dari 60 bahasa, diterjemahkan oleh komunitas global melalui Crowdin. Aplikasi Lintas Platform -Amankan dan bagikan data sensitif dalam Brankas Bitwarden Anda dari sebarang peramban, ponsel, atau sistem operasi desktop, dan lebih banyak lagi. +Amankan dan bagikan data sensitif dalam Brankas Bitwarden Anda dari peramban apa pun, ponsel, atau sistem operasi desktop, dan lebih banyak lagi. Bitwarden mengamankan lebih dari sekedar kata sandi -Solusi pengelolaan pengenal terenkripsi ujung-ke-ujung dari Bitwarden memberdayakan organisasi untuk mengamankan segalanya, termasuk rahasia pengembang dan pengalaman kunci sandi. Kunjungi bitwarden.com untuk mempelajari lebih lanjut tentang Pengelola Rahasia Bitwarden dan passwordless.dev Bitwarden! +Solusi pengelolaan identitas terenkripsi ujung-ke-ujung dari Bitwarden memberdayakan organisasi untuk mengamankan segalanya, termasuk rahasia pengembang dan kunci sandi. Kunjungi bitwarden.com untuk mempelajari lebih lanjut tentang Pengelola Rahasia Bitwarden dan passwordless.dev Bitwarden! diff --git a/apps/cli/package.json b/apps/cli/package.json index a4ff56206e4..4964cd4e403 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2025.7.0", + "version": "2025.8.0", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/src/admin-console/commands/confirm.command.ts b/apps/cli/src/admin-console/commands/confirm.command.ts index 1c900511499..adae5091172 100644 --- a/apps/cli/src/admin-console/commands/confirm.command.ts +++ b/apps/cli/src/admin-console/commands/confirm.command.ts @@ -1,13 +1,23 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { firstValueFrom, map, switchMap } from "rxjs"; + import { OrganizationUserApiService, OrganizationUserConfirmRequest, } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { EncString } from "@bitwarden/sdk-internal"; import { Response } from "../../models/response"; @@ -17,6 +27,9 @@ export class ConfirmCommand { private keyService: KeyService, private encryptService: EncryptService, private organizationUserApiService: OrganizationUserApiService, + private accountService: AccountService, + private configService: ConfigService, + private i18nService: I18nService, ) {} async run(object: string, id: string, cmdOptions: Record): Promise { @@ -44,7 +57,14 @@ export class ConfirmCommand { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } try { - const orgKey = await this.keyService.getOrgKey(options.organizationId); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null), + ), + ); + if (orgKey == null) { throw new Error("No encryption key for this organization."); } @@ -60,6 +80,11 @@ export class ConfirmCommand { const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey); const req = new OrganizationUserConfirmRequest(); req.key = key.encryptedString; + if ( + await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) + ) { + req.defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey); + } await this.organizationUserApiService.postOrganizationUserConfirm( options.organizationId, id, @@ -70,6 +95,12 @@ export class ConfirmCommand { return Response.error(e); } } + + private async getEncryptedDefaultUserCollectionName(orgKey: OrgKey): Promise { + const defaultCollectionName = this.i18nService.t("myItems"); + const encrypted = await this.encryptService.encryptString(defaultCollectionName, orgKey); + return encrypted.encryptedString; + } } class Options { diff --git a/apps/cli/src/base-program.ts b/apps/cli/src/base-program.ts index 5719f78c1b9..5957f08de89 100644 --- a/apps/cli/src/base-program.ts +++ b/apps/cli/src/base-program.ts @@ -129,7 +129,7 @@ export abstract class BaseProgram { if (!userId) { fail(); } - const authed = await this.serviceContainer.stateService.getIsAuthenticated({ userId }); + const authed = await firstValueFrom(this.serviceContainer.tokenService.hasAccessToken$(userId)); if (!authed) { fail(); } diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index c2881568656..2e273d041d6 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { CollectionRequest } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -14,6 +14,7 @@ import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; import { FolderExport } from "@bitwarden/common/models/export/folder.export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -201,7 +202,13 @@ export class EditCommand { return Response.badRequest("`organizationid` option does not match request object."); } try { - const orgKey = await this.keyService.getOrgKey(req.organizationId); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null), + ), + ); if (orgKey == null) { throw new Error("No encryption key for this organization."); } @@ -218,14 +225,15 @@ export class EditCommand { : req.users.map( (u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage), ); - const request = new CollectionRequest(); - request.name = (await this.encryptService.encryptString(req.name, orgKey)).encryptedString; - request.externalId = req.externalId; - request.groups = groups; - request.users = users; + const request = new CollectionRequest({ + name: await this.encryptService.encryptString(req.name, orgKey), + externalId: req.externalId, + users, + groups, + }); + const response = await this.apiService.putCollection(req.organizationId, id, request); - const view = CollectionExport.toView(req); - view.id = response.id; + const view = CollectionExport.toView(req, response.id); const res = new OrganizationCollectionResponse(view, groups, users); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 756316cba43..58181cff6ce 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -13,7 +13,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { CardExport } from "@bitwarden/common/models/export/card.export"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; @@ -452,6 +451,7 @@ export class GetCommand extends DownloadCommand { const orgKeys = await firstValueFrom(this.keyService.activeUserOrgKeys$); decCollection = await collection.decrypt( orgKeys[collection.organizationId as OrganizationId], + this.encryptService, ); } } else if (id.trim() !== "") { @@ -485,15 +485,21 @@ export class GetCommand extends DownloadCommand { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } try { - const orgKey = await this.keyService.getOrgKey(options.organizationId); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null), + ), + ); if (orgKey == null) { throw new Error("No encryption key for this organization."); } const response = await this.apiService.getCollectionAccessDetails(options.organizationId, id); - const decCollection = new CollectionView(response); - decCollection.name = await this.encryptService.decryptString( - new EncString(response.name), + const decCollection = await CollectionView.fromCollectionAccessDetails( + response, + this.encryptService, orgKey, ); const groups = diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 94abd97d6eb..d8b4cfcfd10 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -211,7 +211,9 @@ export class ListCommand { } const collections = response.data .filter((c) => c.organizationId === options.organizationId) - .map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse))); + .map((r) => + Collection.fromCollectionData(new CollectionData(r as ApiCollectionDetailsResponse)), + ); const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId)); if (orgKeys == null) { throw new Error("Organization keys not found."); diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 801f505f1ae..c0ec37d3c9c 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -1,3 +1,6 @@ +import http from "node:http"; +import net from "node:net"; + import * as koaRouter from "@koa/router"; import { OptionValues } from "commander"; import * as koa from "koa"; @@ -50,11 +53,33 @@ export class ServeCommand { this.serveConfigurator.configureRouter(router); - server - .use(router.routes()) - .use(router.allowedMethods()) - .listen(port, hostname === "all" ? null : hostname, () => { + server.use(router.routes()).use(router.allowedMethods()); + + if (hostname.startsWith("fd+connected://")) { + const fd = parseInt(hostname.slice("fd+connected://".length)); + const httpServer = http.createServer(server.callback()); + const socket = new net.Socket({ fd: fd, readable: true, writable: true }); + // allow idle sockets, incomplete handshakes and slow requests + httpServer.keepAliveTimeout = 0; + httpServer.headersTimeout = 0; + httpServer.timeout = 0; + socket.pause(); + httpServer.emit("connection", socket); + socket.resume(); // Let the HTTP parser start reading + } else if (hostname.startsWith("fd+listening://")) { + const fd = parseInt(hostname.slice("fd+listening://".length)); + server.listen({ fd }, () => { + this.serviceContainer.logService.info("Listening on " + hostname); + }); + } else if (hostname.startsWith("unix://")) { + const socketPath = hostname.slice("unix://".length); + server.listen(socketPath, () => { + this.serviceContainer.logService.info("Listening on " + hostname); + }); + } else { + server.listen(port, hostname === "all" ? null : hostname, () => { this.serviceContainer.logService.info("Listening on " + hostname + ":" + port); }); + } } } diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index 815939c0c95..cb7f89781dd 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -215,5 +215,8 @@ }, "youHaveBeenLoggedOut": { "message": "You have been logged out." + }, + "myItems": { + "message": "My Items" } } diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index a460fa270a8..6ae2776eae7 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -107,7 +107,8 @@ export class OssServeConfigurator { ); this.generateCommand = new GenerateCommand( this.serviceContainer.passwordGenerationService, - this.serviceContainer.stateService, + this.serviceContainer.tokenService, + this.serviceContainer.accountService, ); this.syncCommand = new SyncCommand(this.serviceContainer.syncService); this.statusCommand = new StatusCommand( @@ -131,6 +132,9 @@ export class OssServeConfigurator { this.serviceContainer.keyService, this.serviceContainer.encryptService, this.serviceContainer.organizationUserApiService, + this.serviceContainer.accountService, + this.serviceContainer.configService, + this.serviceContainer.i18nService, ); this.restoreCommand = new RestoreCommand( this.serviceContainer.cipherService, @@ -414,14 +418,18 @@ export class OssServeConfigurator { } protected async errorIfLocked(res: koa.Response) { - const authed = await this.serviceContainer.stateService.getIsAuthenticated(); + const userId = await firstValueFrom( + this.serviceContainer.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + + const authed = + userId != null || + (await firstValueFrom(this.serviceContainer.tokenService.hasAccessToken$(userId))); + if (!authed) { this.processResponse(res, Response.error("You are not logged in.")); return true; } - const userId = await firstValueFrom( - this.serviceContainer.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); if (await this.serviceContainer.keyService.hasUserKey(userId)) { return false; } diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 468901282b4..4d541739aab 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import * as chalk from "chalk"; import { program, Command, OptionValues } from "commander"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, of, switchMap } from "rxjs"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -129,7 +129,17 @@ export class Program extends BaseProgram { "Path to a file containing your password as its first line", ) .option("--check", "Check login status.", async () => { - const authed = await this.serviceContainer.stateService.getIsAuthenticated(); + const authed = await firstValueFrom( + this.serviceContainer.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account == null) { + return of(false); + } + + return this.serviceContainer.tokenService.hasAccessToken$(account.id); + }), + ), + ); if (authed) { const res = new MessageResponse("You are logged in!", null); this.processResponse(Response.success(res), true); @@ -350,7 +360,8 @@ export class Program extends BaseProgram { .action(async (options) => { const command = new GenerateCommand( this.serviceContainer.passwordGenerationService, - this.serviceContainer.stateService, + this.serviceContainer.tokenService, + this.serviceContainer.accountService, ); const response = await command.run(options); this.processResponse(response); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 0ec24768b79..e82ceb5a6e9 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -60,6 +60,10 @@ import { import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { + DefaultKeyGenerationService, + KeyGenerationService, +} from "@bitwarden/common/key-management/crypto"; import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation"; @@ -81,14 +85,10 @@ import { EnvironmentService, RegionConfig, } from "@bitwarden/common/platform/abstractions/environment.service"; -import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { MessageSender } from "@bitwarden/common/platform/messaging"; -import { Account } from "@bitwarden/common/platform/models/domain/account"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { TaskSchedulerService, DefaultTaskSchedulerService, @@ -99,23 +99,23 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.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"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ActiveUserStateProvider, + DefaultStateService, DerivedStateProvider, GlobalStateProvider, SingleUserStateProvider, StateEventRunnerService, StateProvider, + StateService, } from "@bitwarden/common/platform/state"; /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */ import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; @@ -210,6 +210,7 @@ export class ServiceContainer { secureStorageService: NodeEnvSecureStorageService; memoryStorageService: MemoryStorageService; memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; + migrationRunner: MigrationRunner; i18nService: I18nService; platformUtilsService: CliPlatformUtilsService; keyService: KeyService; @@ -239,7 +240,7 @@ export class ServiceContainer { individualExportService: IndividualVaultExportServiceAbstraction; organizationExportService: OrganizationVaultExportServiceAbstraction; searchService: SearchService; - keyGenerationService: KeyGenerationServiceAbstraction; + keyGenerationService: KeyGenerationService; cryptoFunctionService: NodeCryptoFunctionService; encryptService: EncryptServiceImplementation; authService: AuthService; @@ -377,8 +378,10 @@ export class ServiceContainer { this.singleUserStateProvider, ); + const activeUserAccessor = new DefaultActiveUserAccessor(this.accountService); + this.activeUserStateProvider = new DefaultActiveUserStateProvider( - new DefaultActiveUserAccessor(this.accountService), + activeUserAccessor, this.singleUserStateProvider, ); @@ -397,7 +400,7 @@ export class ServiceContainer { process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[], ); - this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); + this.keyGenerationService = new DefaultKeyGenerationService(this.cryptoFunctionService); this.tokenService = new TokenService( this.singleUserStateProvider, @@ -410,23 +413,17 @@ export class ServiceContainer { logoutCallback, ); - const migrationRunner = new MigrationRunner( + this.migrationRunner = new MigrationRunner( this.storageService, this.logService, new MigrationBuilderService(), ClientType.Cli, ); - this.stateService = new StateService( + this.stateService = new DefaultStateService( this.storageService, this.secureStorageService, - this.memoryStorageService, - this.logService, - new StateFactory(GlobalState, Account), - this.accountService, - this.environmentService, - this.tokenService, - migrationRunner, + activeUserAccessor, ); this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider); @@ -530,10 +527,7 @@ export class ServiceContainer { this.authService, ); - this.domainSettingsService = new DefaultDomainSettingsService( - this.stateProvider, - this.configService, - ); + this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); this.fileUploadService = new FileUploadService(this.logService, this.apiService); @@ -714,7 +708,6 @@ export class ServiceContainer { this.apiService, this.i18nService, this.searchService, - this.stateService, this.autofillSettingsService, this.encryptService, this.cipherFileUploadService, @@ -765,6 +758,7 @@ export class ServiceContainer { this.messagingService, this.searchService, this.stateService, + this.tokenService, this.authService, this.vaultTimeoutSettingsService, this.stateEventRunnerService, @@ -791,7 +785,6 @@ export class ServiceContainer { this.sendService, this.logService, this.keyConnectorService, - this.stateService, this.providerService, this.folderApiService, this.organizationService, @@ -904,7 +897,8 @@ export class ServiceContainer { await this.stateEventRunnerService.handleEvent("logout", userId as UserId); - await this.stateService.clean(); + await this.stateService.clean({ userId: userId }); + await this.tokenService.clearAccessToken(userId); await this.accountService.clean(userId as UserId); await this.accountService.switchAccount(null); process.env.BW_SESSION = undefined; @@ -918,7 +912,8 @@ export class ServiceContainer { await this.sdkLoadService.loadAndInit(); await this.storageService.init(); - await this.stateService.init(); + + await this.migrationRunner.run(); this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); diff --git a/apps/cli/src/tools/generate.command.ts b/apps/cli/src/tools/generate.command.ts index 64c6118a0c6..1d8a8690ab3 100644 --- a/apps/cli/src/tools/generate.command.ts +++ b/apps/cli/src/tools/generate.command.ts @@ -1,6 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { firstValueFrom, of, switchMap } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { DefaultPasswordGenerationOptions, DefaultPassphraseGenerationOptions, @@ -17,7 +20,8 @@ import { CliUtils } from "../utils"; export class GenerateCommand { constructor( private passwordGenerationService: PasswordGenerationServiceAbstraction, - private stateService: StateService, + private tokenService: TokenService, + private accountService: AccountService, ) {} async run(cmdOptions: Record): Promise { @@ -38,7 +42,18 @@ export class GenerateCommand { ambiguous: !normalizedOptions.ambiguous, }; - const enforcedOptions = (await this.stateService.getIsAuthenticated()) + const shouldEnforceOptions = await firstValueFrom( + this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account == null) { + return of(false); + } + + return this.tokenService.hasAccessToken$(account.id); + }), + ), + ); + const enforcedOptions = shouldEnforceOptions ? (await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options))[0] : options; diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index 82699e273c3..2ea73f8c5c8 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -55,10 +55,13 @@ export class SendProgram extends BaseProgram { "optional password to access this Send. Can also be specified in JSON.", ).conflicts("email"), ) - .option( - "--email ", - "optional emails to access this Send. Can also be specified in JSON.", - parseEmail, + .addOption( + new Option( + "--email ", + "optional emails to access this Send. Can also be specified in JSON.", + ) + .argParser(parseEmail) + .hideHelp(), ) .option("-a, --maxAccessCount ", "The amount of max possible accesses.") .option("--hidden", "Hide in web by default. Valid only if --file is not set.") diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index bdcc52393ca..5b35f6b0499 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -432,6 +432,9 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.keyService, this.serviceContainer.encryptService, this.serviceContainer.organizationUserApiService, + this.serviceContainer.accountService, + this.serviceContainer.configService, + this.serviceContainer.i18nService, ); const response = await command.run(object, id, cmd); this.processResponse(response); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 33ec52eeca8..cbaae4d2544 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -233,14 +233,14 @@ export class CreateCommand { : req.users.map( (u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage), ); - const request = new CollectionRequest(); - request.name = (await this.encryptService.encryptString(req.name, orgKey)).encryptedString; - request.externalId = req.externalId; - request.groups = groups; - request.users = users; + const request = new CollectionRequest({ + name: await this.encryptService.encryptString(req.name, orgKey), + externalId: req.externalId, + groups, + users, + }); const response = await this.apiService.postCollection(req.organizationId, request); - const view = CollectionExport.toView(req); - view.id = response.id; + const view = CollectionExport.toView(req, response.id); const res = new OrganizationCollectionResponse(view, groups, users); return Response.success(res); } catch (e) { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 37650c08b95..42eb7017e03 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2025.8.0", + "version": "2025.8.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 30ae906c98d..db079cd60f9 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -2,10 +2,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; -import { - DesktopDefaultOverlayPosition, - EnvironmentSelectorComponent, -} from "@bitwarden/angular/auth/components/environment-selector.component"; +import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/environment-selector/environment-selector.component"; import { authGuard, lockGuard, @@ -174,9 +171,6 @@ const routes: Routes = [ path: "", component: EnvironmentSelectorComponent, outlet: "environment-selector", - data: { - overlayPosition: DesktopDefaultOverlayPosition, - }, }, ], }, @@ -205,9 +199,6 @@ const routes: Routes = [ path: "", component: EnvironmentSelectorComponent, outlet: "environment-selector", - data: { - overlayPosition: DesktopDefaultOverlayPosition, - }, }, ], }, @@ -228,9 +219,6 @@ const routes: Routes = [ path: "", component: EnvironmentSelectorComponent, outlet: "environment-selector", - data: { - overlayPosition: DesktopDefaultOverlayPosition, - }, }, ], }, @@ -265,9 +253,6 @@ const routes: Routes = [ path: "", component: EnvironmentSelectorComponent, outlet: "environment-selector", - data: { - overlayPosition: DesktopDefaultOverlayPosition, - }, }, ], }, diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 8c74624414b..197290cf690 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -40,6 +40,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -175,6 +176,7 @@ export class AppComponent implements OnInit, OnDestroy { private readonly destroyRef: DestroyRef, private readonly documentLangSetter: DocumentLangSetter, private restrictedItemTypesService: RestrictedItemTypesService, + private readonly tokenService: TokenService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -684,6 +686,7 @@ export class AppComponent implements OnInit, OnDestroy { await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); await this.stateService.clean({ userId: userBeingLoggedOut }); + await this.tokenService.clearAccessToken(userBeingLoggedOut); await this.accountService.clean(userBeingLoggedOut); // HACK: Wait for the user logging outs authentication status to transition to LoggedOut diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 0738aaba295..6b511ff366d 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -15,6 +15,7 @@ import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk- import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/platform/sync"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; @@ -52,6 +53,7 @@ export class InitService { private autotypeService: DesktopAutotypeService, private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, + private readonly migrationRunner: MigrationRunner, ) {} init() { @@ -59,7 +61,7 @@ export class InitService { await this.sdkLoadService.loadAndInit(); await this.sshAgentService.init(); this.nativeMessagingService.init(); - await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process + await this.migrationRunner.waitForCompletion(); // Desktop will run migrations in the main process const accounts = await firstValueFrom(this.accountService.accounts$); const setUserKeyInMemoryPromises = []; diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 95d1f4643fa..3d65094ba60 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -51,6 +51,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service"; @@ -67,7 +68,6 @@ import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } fro import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService, LogService as LogServiceAbstraction, @@ -304,7 +304,7 @@ const safeProviders: SafeProvider[] = [ deps: [ PinServiceAbstraction, InternalMasterPasswordServiceAbstraction, - KeyGenerationServiceAbstraction, + KeyGenerationService, CryptoFunctionServiceAbstraction, EncryptService, PlatformUtilsServiceAbstraction, diff --git a/apps/desktop/src/auth/login/login.module.ts b/apps/desktop/src/auth/login/login.module.ts index 601c71e00b1..904d1b34373 100644 --- a/apps/desktop/src/auth/login/login.module.ts +++ b/apps/desktop/src/auth/login/login.module.ts @@ -1,13 +1,10 @@ import { NgModule } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; - import { SharedModule } from "../../app/shared/shared.module"; @NgModule({ imports: [SharedModule, RouterModule], - declarations: [EnvironmentSelectorComponent], exports: [], }) export class LoginModule {} diff --git a/apps/desktop/src/key-management/electron-key.service.spec.ts b/apps/desktop/src/key-management/electron-key.service.spec.ts index af01bf51c15..2d60c47217d 100644 --- a/apps/desktop/src/key-management/electron-key.service.spec.ts +++ b/apps/desktop/src/key-management/electron-key.service.spec.ts @@ -1,10 +1,10 @@ import { mock } from "jest-mock-extended"; +import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.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"; diff --git a/apps/desktop/src/key-management/electron-key.service.ts b/apps/desktop/src/key-management/electron-key.service.ts index 48ccd3f27fd..59295b2ca21 100644 --- a/apps/desktop/src/key-management/electron-key.service.ts +++ b/apps/desktop/src/key-management/electron-key.service.ts @@ -1,9 +1,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.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"; @@ -65,7 +65,7 @@ export class ElectronKeyService extends DefaultKeyService { protected override async getKeyFromStorage( keySuffix: KeySuffixOptions, - userId?: UserId, + userId: UserId, ): Promise { return await super.getKeyFromStorage(keySuffix, userId); } diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 39c58f4b26d..3a483445ae2 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Tweestapaantekening" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Kluis-uittel" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Kies wanneer u kluis sal uittel en die gekose aksie sal uitvoer." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "U kluisuittelling oorskry die beperkinge wat deur u organisasie daargestel is." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Tokkel karakter telling", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Toestel Tiepe" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "Aantekening gebevestig vir $EMAIL$ op $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Jy het 'n aantekenings poging van 'n ander toestel gekeer. As dit inderdaad jy was, probeer om weer met die toestel aan te teken." - }, "justNow": { "message": "Nou net" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index f93a8c70214..ebaef5f120c 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "تسجيل الدخول بخطوتين" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "مهلة الخزانة" }, "vaultTimeout1": { "message": "المهلة" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "اختر وقت انتهاء مهلة خزانتك وقم بتنفيذ الإجراء المحدد." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "مهلة خزانتك تتجاوز القيود التي تضعها مؤسستك." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "تم قبول الدعوة" }, @@ -2992,9 +3027,6 @@ "message": "تبديل عدد الأحرف", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "نوع الجهاز" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "تم تأكيد تسجيل الدخول لـ $EMAIL$ في $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "لقد رفضت محاولة تسجيل الدخول من جهاز آخر. إذا كان هذا بالفعل أنت ، حاول تسجيل الدخول مرة أخرى باستخدام الجهاز." - }, "justNow": { "message": "للتو الآن" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index f498aae4180..d8dd0c73339 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -42,7 +42,7 @@ "message": "Seyfdə axtar" }, "resetSearch": { - "message": "Reset search" + "message": "Axtarışı sıfırla" }, "addItem": { "message": "Element əlavə et" @@ -138,7 +138,7 @@ "message": "Lövhəyə kopyalananda kiçilt" }, "minimizeOnCopyToClipboardDesc": { - "message": "Bir elementin datasını lövhəyə kopyalayarkən tətbiqi kiçilt." + "message": "Bir elementin verilərini lövhəyə kopyalayarkən tətbiqi kiçilt." }, "toggleVisibility": { "message": "Görünməni aç/bağla" @@ -241,7 +241,7 @@ "message": "SSH tələblərini birbaşa Bitwarden seyfinizdən imzalamaq üçün SSH agentini fəallaşdırın." }, "enableSshAgentHelp": { - "message": "SSH agenti, SSH tələblərini birbaşa Bitwarden seyfinizdən imzalamağa imkan verən developerlərə yönəlmiş bir xidmətdir." + "message": "SSH agenti, SSH tələblərini birbaşa Bitwarden seyfinizdən imzalamağa imkan verən gəlişdiricilərə yönəlmiş bir xidmətdir." }, "sshAgentPromptBehavior": { "message": "SSH agentini istifadə edərkən səlahiyyətləndirmə üçün soruş" @@ -461,10 +461,10 @@ "message": "Əlavə et" }, "textHelpText": { - "message": "Təhlükəsizlik sualları kimi datalar üçün mətn xanalarını istifadə edin" + "message": "Təhlükəsizlik sualları kimi verilər üçün mətn xanalarını istifadə edin" }, "hiddenHelpText": { - "message": "Parol kimi həssas datalar üçün gizli xanaları istifadə edin" + "message": "Parol kimi həssas verilər üçün gizli xanaları istifadə edin" }, "checkBoxHelpText": { "message": "\"E-poçtu xatırla\" kimi formun təsdiq qutusunu avto-doldurmaq istəyirsinizsə təsdiq qutularını istifadə edin" @@ -727,7 +727,7 @@ "message": "Qovluq silindi" }, "loginOrCreateNewAccount": { - "message": "Güvənli seyfinizə müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." + "message": "Güvənli seyfinizə erişmək üçün giriş edin və ya yeni bir hesab yaradın." }, "createAccount": { "message": "Hesab yarat" @@ -772,7 +772,7 @@ "message": "Ana parol" }, "masterPassDesc": { - "message": "Ana parol, seyfinizə müraciət etmək üçün istifadə edəcəyiniz paroldur. Ana parolu yadda saxlamaq çox vacibdir. Unutsanız, parolu bərpa etməyin heç bir yolu yoxdur." + "message": "Ana parol, seyfinizə erişmək üçün istifadə edəcəyiniz paroldur. Ana parolu yadda saxlamaq çox vacibdir. Unutsanız, parolu bərpa etməyin heç bir yolu yoxdur." }, "masterPassHintDesc": { "message": "Ana parol məsləhəti, unutduğunuz parolunuzu xatırlamağınıza kömək edir." @@ -955,7 +955,7 @@ "message": "Güvənlik açarını kompüterinizin USB portuna taxın. Düyməsi varsa toxunun." }, "recoveryCodeDesc": { - "message": "İki faktorlu provayderlərinizə müraciəti itirmisiniz? Geri qaytarma kodunuzu istifadə edərək hesabınızdakı bütün iki faktorlu provayderləri söndürə bilərsiniz." + "message": "İki faktorlu provayderlərinizə erişə bilmirsiniz? Geri qaytarma kodunuzu istifadə edərək hesabınızdakı bütün iki faktorlu provayderləri söndürə bilərsiniz." }, "recoveryCodeTitle": { "message": "Bərpa kodu" @@ -971,7 +971,7 @@ "message": "Yubico OTP güvənlik açarı" }, "yubiKeyDesc": { - "message": "Hesabınıza müraciət etmək üçün bir YubiKey istifadə edin. YubiKey 4, 4 Nano, 4C və NEO cihazları ilə işləyir." + "message": "Hesabınıza erişmək üçün bir YubiKey istifadə edin. YubiKey 4, 4 Nano, 4C və NEO cihazları ilə işləyir." }, "duoDescV2": { "message": "Duo Security tərəfindən yaradılan kodu daxil edin.", @@ -994,7 +994,7 @@ "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Hesabınıza müraciət etmək üçün istənilən bir WebAuthn fəallaşdırılmış güvənlik açarı istifadə edin." + "message": "Hesabınıza erişmək üçün istənilən WebAuthn uyumlu güvənlik açarını istifadə edin." }, "emailTitle": { "message": "E-poçt" @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "İki mərhələli giriş" }, + "vaultTimeoutHeader": { + "message": "Seyf vaxtının bitməsi" + }, "vaultTimeout": { "message": "Seyf vaxtının bitməsi" }, "vaultTimeout1": { "message": "Vaxt bitmə" }, + "vaultTimeoutAction1": { + "message": "Vaxt bitmə əməliyyatı" + }, "vaultTimeoutDesc": { "message": "Seyfinizin vaxt bitmə əməliyyatını nə vaxt icra edəcəyini seçin." }, @@ -1470,7 +1476,7 @@ "message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri." }, "premiumSignUpReports": { - "message": "Seyfinizi güvəndə saxlamaq üçün parol gigiyenası, hesab sağlamlığı və data pozuntusu hesabatları." + "message": "Seyfinizi güvəndə saxlamaq üçün parol gigiyenası, hesab sağlamlığı və veri pozuntusu hesabatları." }, "premiumSignUpTotp": { "message": "Seyfinizdəki girişlər üçün TOTP doğrulama kodu (2FA) yaradıcısı." @@ -1566,7 +1572,7 @@ "message": "Təkrar yüklə" }, "toggleDevTools": { - "message": "Tərtibatçı alətlərini aç/bağla" + "message": "Gəlişdirici alətlərini aç/bağla" }, "minimize": { "message": "Kiçilt", @@ -1611,7 +1617,7 @@ "message": "Uğurla kopyalandı" }, "errorRefreshingAccessToken": { - "message": "Müraciət tokeni təzələmə xətası" + "message": "Erişim tokeni təzələmə xətası" }, "errorRefreshingAccessTokenDesc": { "message": "Təzələmə tokeni və ya API açarlar tapılmadı. Lütfən çıxış edib yenidən giriş etməyə çalışın." @@ -1626,7 +1632,7 @@ "message": "Parolunuzun oğurlanıb oğurlanmadığını yoxlayın." }, "passwordExposed": { - "message": "Bu parol, məlumat pozuntularında $VALUE$ dəfə üzə çıxıb. Dəyişdirməyi məsləhət görürük.", + "message": "Bu parol, veri pozuntularında $VALUE$ dəfə üzə çıxıb. Dəyişdirməyi məsləhət görürük.", "placeholders": { "value": { "content": "$1", @@ -1635,7 +1641,7 @@ } }, "passwordSafe": { - "message": "Bu parol, məlumat pozuntularında qeydə alınmayıb. Rahatlıqla istifadə edə bilərsiniz." + "message": "Bu parol, veri pozuntularında qeydə alınmayıb. Rahatlıqla istifadə edə bilərsiniz." }, "baseDomain": { "message": "Baza domeni", @@ -1756,10 +1762,10 @@ "message": "Seyfi xaricə köçürməyi təsdiqlə" }, "exportWarningDesc": { - "message": "Xaricə köçürdüyünüz bu fayldakı datanız şifrələnməmiş formatdadır. Bu faylı güvənli olmayan kanallar (e-poçt kimi) üzərində saxlamamalı və ya göndərməməlisiniz. İşiniz bitdikdən sonra faylı dərhal silin." + "message": "Bu xaricə köçürmədəki seyf veriləriniz şifrələnməmiş formatdadır. Bu xaricə köçürülmüş faylı güvənli olmayan kanallar (e-poçt kimi) üzərində saxlamamalı və ya göndərməməlisiniz. İşiniz bitdikdən sonra faylı dərhal silin." }, "encExportKeyWarningDesc": { - "message": "Xaricə köçürdüyünüz bu fayldakı data, hesabınızın şifrələmə açarı istifadə edilərək şifrələnir. Hesabınızın şifrələmə açarını dəyişdirsəniz, bu faylın şifrəsini aça bilməyəcəksiniz və onu yenidən xaricə köçürməli olacaqsınız." + "message": "Bu xaricə köçürmə, verilərinizi hesabınızın şifrələmə açarını istifadə edərək şifrələyir. Hesabınızın şifrələmə açarını dəyişdirsəniz, bu faylın şifrəsini aça bilməyəcəksiniz və onu yenidən xaricə köçürməli olacaqsınız." }, "encExportAccountWarningDesc": { "message": "Hesab şifrələmə açarları, hər Bitwarden istifadəçi hesabı üçün unikaldır, buna görə də şifrələnmiş bir xaricə köçürməni, fərqli bir hesaba köçürə bilməzsiniz." @@ -1847,7 +1853,7 @@ "message": "Hesabı sil" }, "deleteAccountDesc": { - "message": "Hesabınızı və bütün seyf datasını silmək üçün aşağıdakı addımları izləyin." + "message": "Hesabınızı və bütün seyf verilərini silmək üçün aşağıdakı addımları izləyin." }, "deleteAccountWarning": { "message": "Hesabınızı silmək birdəfəlik prosesdir. Bu əməliyyatın geri dönüşü yoxdur." @@ -1862,7 +1868,7 @@ "message": "Hesab silindi" }, "accountDeletedDesc": { - "message": "Hesabınız bağlandı və bütün əlaqəli datalar silindi." + "message": "Hesabınız bağlanıb və bütün əlaqələndirilmiş verilər silinib." }, "preferences": { "message": "Tərcihlər" @@ -1914,10 +1920,10 @@ "message": "Seyf vaxtının bitmə əməliyyatı" }, "vaultTimeoutActionLockDesc": { - "message": "Seyfinizə təkrar müraciət etmək üçün ana parol və ya digər kilid açma üsulu tələb olunur." + "message": "Seyfinizə təkrar erişmək üçün ana parol və ya digər kilid açma üsulu tələb olunur." }, "vaultTimeoutActionLogOutDesc": { - "message": "Seyfinizə təkrar müraciət etmək üçün təkrar kimlik doğrulama tələb olunur." + "message": "Seyfinizə təkrar erişmək üçün təkrar kimlik doğrulama tələb olunur." }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Seyf vaxt bitmə əməliyyatınızı dəyişdirmək üçün bir kilid açma üsulu qurun." @@ -1949,7 +1955,7 @@ "message": "Birdəfəlik sil" }, "vaultTimeoutLogOutConfirmation": { - "message": "Çıxış etdikdə, seyfinizə bütün müraciətiniz dayanacaq və vaxt bitməsindən sonra onlayn kimlik doğrulaması tələb olunacaq. Bu ayarı istifadə etmək istədiyinizə əminsiniz?" + "message": "Çıxış etdikdə, seyfinizə erişiminiz tamamilə dayanacaq və vaxt bitdikdən sonra onlayn kimlik doğrulaması tələb olunacaq. Bu ayarı istifadə etmək istədiyinizə əminsiniz?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "Vaxt bitmə əməliyyat təsdiqi" @@ -2142,7 +2148,7 @@ "message": "Masaüstü tətbiqi ilə brauzer arasında bağlantı qurarkən barmaq izi ifadəsinin təsdiqlənməsini tələb edərək əlavə bir güvənlik qatını fəallaşdıra bilərsiniz. Bu, hər bağlantı qurulanda istifadəçi müdaxiləsi və doğrulama tələb edir." }, "enableHardwareAcceleration": { - "message": "Avadanlıq sürətləndirməsini istifadə et" + "message": "Donanım sürətləndirməsini istifadə et" }, "enableHardwareAccelerationDesc": { "message": "İlkin olaraq bu ayar AÇIQDIR. Yalnız qrafik problemlərlə üzləşsəniz SÖNDÜRÜN. Yenidən başlatma tələb olunur." @@ -2242,26 +2248,26 @@ "message": "Bitmə tarixi" }, "expirationDateDesc": { - "message": "Əgər ayarlanıbsa, göstərilən tarix və vaxtda \"Send\"ə müraciət başa çatacaq.", + "message": "Əgər ayarlanıbsa, göstərilən tarix və vaxtda \"Send\"ə erişim başa çatacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCount": { - "message": "Maksimal müraciət sayı", + "message": "Maksimum erişim sayı", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "maxAccessCountDesc": { - "message": "Əgər ayarlanıbsa, istifadəçilər maksimal müraciət sayına çatdıqdan sonra bu \"Send\"ə müraciət edə bilməyəcək.", + "message": "Əgər ayarlanıbsa, istifadəçilər maksimum erişim sayına çatdıqdan sonra bu \"Send\"ə erişə bilməyəcək.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "currentAccessCount": { - "message": "Hazırkı müraciət sayı" + "message": "Hazırkı erişim sayı" }, "disableSend": { - "message": "Heç kimin müraciət edə bilməməsi üçün bu \"Send\"i sıradan çıxart.", + "message": "Heç kimin erişə bilməməsi üçün bu \"Send\"i deaktiv et.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { - "message": "İstəyinizə görə istifadəçilərdən bu \"Send\"ə müraciət edərkən parol tələb edə bilərsiniz.", + "message": "İstəyinizə görə istifadəçilərdən bu \"Send\"ə erişməsi üçün parol tələb edə bilərsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2277,7 +2283,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "textHiddenByDefault": { - "message": "\"Send\"ə müraciət edəndə ilkin olaraq mətni gizlədin", + "message": "\"Send\"ə erişərkən ilkin olaraq mətni gizlədin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { @@ -2359,7 +2365,7 @@ "message": "Parolu çıxartmaq istədiyinizə əminsiniz?" }, "maxAccessCountReached": { - "message": "Maksimal müraciət sayına çatıldı" + "message": "Maksimum erişim sayına çatıldı" }, "expired": { "message": "Müddəti bitib" @@ -2410,10 +2416,10 @@ "message": "Ana parolu güncəllə" }, "updateMasterPasswordWarning": { - "message": "Ana parolunuz təzəlikcə təşkilatınızdakı bir inzibatçı tərəfindən dəyişdirildi. Seyfə müraciət etmək üçün onu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış edəcəksiniz və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." + "message": "Ana parolunuz təzəlikcə təşkilatınızdakı bir inzibatçı tərəfindən dəyişdirildi. Seyfə erişmək üçün onu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış edəcəksiniz və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." }, "updateWeakMasterPasswordWarning": { - "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Seyfə müraciət üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." + "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Seyfə erişmək üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." }, "changePasswordWarning": { "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saat ərzində çıxış sonlandırılacaq." @@ -2425,7 +2431,7 @@ "message": "Ana parolunuz bu təşkilatın tələblərinə cavab vermir. Davam etmək üçün ana parolunuzu dəyişdirin." }, "tdeDisabledMasterPasswordRequired": { - "message": "Təşkilatınız, güvənli cihaz şifrələməsini sıradan çıxartdı. Seyfinizə müraciət etmək üçün lütfən ana parol təyin edin." + "message": "Təşkilatınız, güvənli cihaz şifrələməsini sıradan çıxartdı. Seyfinizə erişmək üçün lütfən ana parol təyin edin." }, "tryAgain": { "message": "Yenidən sına" @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Seyfin bitmə vaxtı, təşkilatınız tərəfindən ayarlanan məhdudiyyətləri aşır." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Müəssisə siyasət tələbləri, vaxt bitmə seçimlərinizə tətbiq edildi" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Təşkilatınızın siyasətləri, icazə verilən maksimum seyf bitmə vaxtını $HOURS$ saat $MINUTES$ dəqiqə olaraq ayarladı.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Vaxt bitməsinə təyin olunan vaxt, təşkilatınız tərəfindən ayarlanan məhdudiyyəti aşır: Maksimum $HOURS$ saat və $MINUTES$ dəqiqə", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Dəvət qəbul edildi" }, @@ -2902,7 +2937,7 @@ "description": "Part of a URL." }, "apiAccessToken": { - "message": "API müraciət tokeni" + "message": "API erişim tokeni" }, "apiKey": { "message": "API açar" @@ -2914,7 +2949,7 @@ "message": "Təşkilat sıradan çıxarıldı." }, "disabledOrganizationFilterError": { - "message": "Sıradan çıxarılmış Təşkilatlardakı elementlərə müraciət edilə bilmir. Kömək üçün Təşkilatınızın sahibi ilə əlaqə saxlayın." + "message": "Fəaliyyəti dayandırılmış təşkilatlardakı elementlərə erişilə bilmir. Kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." }, "neverLockWarning": { "message": "\"Heç vaxt\"i seçmək istədiyinizə əminsiniz? Kilid seçimini \"Heç vaxt\" olaraq ayarlasanız, seyfinizin şifrələmə açarı cihazınızda saxlanılacaq. Bu seçimi istifadə etsəniz, cihazınızı daha yaxşı mühafizə etdiyinizə əmin olmalısınız." @@ -2992,11 +3027,8 @@ "message": "Simvol sayını dəyişdir", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Hesabınıza müraciət etməyə çalışırsınız?" - }, "accessAttemptBy": { - "message": "$EMAIL$ ilə müraciət cəhdi", + "message": "$EMAIL$ ilə erişim cəhdi", "placeholders": { "email": { "content": "$1", @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "$DEVICE$ cihazında $EMAIL$ üçün giriş tələbi təsdiqləndi", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Başqa bir cihazdan giriş cəhdinə rədd cavabı verdiniz. Bu siz idinizsə, cihazla yenidən giriş etməyə çalışın." + }, + "webApp": { + "message": "Veb tətbiq" + }, + "mobile": { + "message": "Mobil", + "description": "Mobile app" + }, + "extension": { + "message": "Uzantı", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Masaüstü", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Giriş tələbi" + }, "deviceType": { "message": "Cihaz növü" }, @@ -3014,26 +3090,10 @@ "message": "Vaxt" }, "confirmAccess": { - "message": "Müraciəti təsdiqlə" + "message": "Erişimi təsdiqlə" }, "denyAccess": { - "message": "Müraciətə rədd cavabı ver" - }, - "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əhdini rədd etdiniz. Bu həqiqətən siz idinizsə, cihazla yenidən giriş etməyə çalışın." + "message": "Erişimə rədd cavabı ver" }, "justNow": { "message": "İndicə" @@ -3054,7 +3114,7 @@ "message": "Bu tələb artıq yararsızdır." }, "confirmAccessAttempt": { - "message": "$EMAIL$ üçün müraciət cəhdini təsdiqlə", + "message": "$EMAIL$ üçün erişim cəhdini təsdiqlə", "placeholders": { "email": { "content": "$1", @@ -3066,7 +3126,7 @@ "message": "Giriş tələb olundu" }, "accountAccessRequested": { - "message": "Tələb olunan hesaba müraciət" + "message": "Hesab erişimi tələb olundu" }, "creatingAccountOn": { "message": "Hesab yaradılır" @@ -3093,16 +3153,16 @@ "message": "İfşa olunmuş ana parol" }, "exposedMasterPasswordDesc": { - "message": "Parol, məlumat pozuntusunda tapıldı. Hesabınızı qorumaq üçün unikal bir parol istifadə edin. İfşa olunmuş bir parol istifadə etmək istədiyinizə əminsiniz?" + "message": "Parol, veri pozuntusunda tapıldı. Hesabınızı qorumaq üçün unikal bir parol istifadə edin. İfşa olunmuş bir parol istifadə etmək istədiyinizə əminsiniz?" }, "weakAndExposedMasterPassword": { "message": "Zəif və ifşa olunmuş ana parol" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Zəif parol məlumat pozuntusunda aşkarlandı və tapıldı. Hesabınızı qorumaq üçün güclü və unikal bir parol istifadə edin. Bu parolu istifadə etmək istədiyinizə əminsiniz?" + "message": "Zəif parol, veri pozuntusunda aşkarlandı və tapıldı. Hesabınızı qorumaq üçün güclü və unikal bir parol istifadə edin. Bu parolu istifadə etmək istədiyinizə əminsiniz?" }, "checkForBreaches": { - "message": "Bu parol üçün bilinən məlumat pozuntularını yoxlayın" + "message": "Bu parol üçün bilinən veri pozuntularını yoxlayın" }, "loggedInExclamation": { "message": "Giriş edildi!" @@ -3111,10 +3171,10 @@ "message": "Vacib:" }, "accessing": { - "message": "Müraciət edilir" + "message": "Erişilir" }, "accessTokenUnableToBeDecrypted": { - "message": "Müraciət tokeninizin şifrəsi açıla bilmədiyi üçün çıxış etdiniz. Bu problemi həll etmək üçün lütfən yenidən giriş edin." + "message": "Erişim tokeninizin şifrəsi açıla bilmədiyi üçün çıxış etdiniz. Bu problemi həll etmək üçün lütfən yenidən giriş edin." }, "refreshTokenSecureStorageRetrievalFailure": { "message": "Təzələmə tokeniniz alına bilmədiyi üçün çıxış etdiniz. Bu problemi həll etmək üçün lütfən yenidən giriş edin." @@ -3181,7 +3241,7 @@ "message": "self-hosted" }, "accessDenied": { - "message": "Müraciət rədd edildi. Bu səhifəyə baxmaq üçün icazəniz yoxdur." + "message": "Erişim rədd edildi. Bu səhifəyə baxmaq üçün icazəniz yoxdur." }, "accountSuccessfullyCreated": { "message": "Hesab uğurla yaradıldı!" @@ -3220,7 +3280,7 @@ "message": "Təşkilata güvənilmir" }, "emergencyAccessTrustWarning": { - "message": "Hesabınızın təhlükəsizliyi üçün yalnız bu istifadəçiyə fövqəladə hal müraciəti icazəsini verdiyinizi və onun barmaq izinin hesabında görünən barmaq izi ilə uyuşduğunu təsdiqləyin" + "message": "Hesabınızın təhlükəsizliyi üçün yalnız bu istifadəçiyə fövqəladə hal erişim icazəsini verdiyinizi və onun barmaq izinin hesabında görünən barmaq izi ilə uyuşduğunu təsdiqləyin" }, "orgTrustWarning": { "message": "Hesabınızın təhlükəsizliyi üçün yalnız bu təşkilatın üzvüsünüzsə, hesab geri qaytarma fəaldırsa və aşağıda görünən barmaq izi təşkilatın barmaq izi ilə uyuşursa davam edin." @@ -3350,14 +3410,14 @@ "message": "Domen ləqəbi" }, "importData": { - "message": "Datanı daxilə köçür", + "message": "Veriləri daxilə köçür", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { "message": "Daxilə köçürmə xətası" }, "importErrorDesc": { - "message": "Daxilə köçürməyə çalışdığınız data ilə bağlı bir problem var. Lütfən mənbə faylınızda aşağıda sadalanan xətaları həll edib yenidən sınayın." + "message": "Daxilə köçürməyə çalışdığınız verilərlə bağlı bir problem var. Lütfən mənbə faylınızda aşağıda sadalanan xətaları həll edib yenidən sınayın." }, "resolveTheErrorsBelowAndTryAgain": { "message": "Aşağıdakı xətaları həll edin və yenidən sınayın." @@ -3366,7 +3426,7 @@ "message": "Açıqlama" }, "importSuccess": { - "message": "Data uğurla daxilə köçürüldü" + "message": "Verilər uğurla daxilə köçürüldü" }, "importSuccessNumberOfItems": { "message": "Cəmi $AMOUNT$ element daxilə köçürüldü.", @@ -3381,7 +3441,7 @@ "message": "Cəmi" }, "importWarning": { - "message": "Datanı $ORGANIZATION$ təşkilatına köçürürsünüz. Datanızı bu təşkilatın üzvləri ilə paylaşa bilərsiniz. Davam etmək istəyirsiniz?", + "message": "Veriləri $ORGANIZATION$ təşkilatına köçürürsünüz. Veriləriniz, bu təşkilatın üzvləri ilə paylaşıla bilər. Davam etmək istəyirsiniz?", "placeholders": { "organization": { "content": "$1", @@ -3408,13 +3468,13 @@ "message": "Duo-nu brauzerdə başlat" }, "importFormatError": { - "message": "Data doğru format edilməyib. Lütfən daxilə köçürmə faylınızı yoxlayıb yenidən sınayın." + "message": "Verilər düzgün format olunmayıb. Lütfən daxilə köçürmə faylınızı yoxlayıb yenidən sınayın." }, "importNothingError": { "message": "Heç nə daxilə köçürülmədi." }, "importEncKeyError": { - "message": "Xaricə köçürülən faylın şifrəsi açılarkən xəta baş verdi. Şifrələmə açarınız, datanı xaricə köçürmək üçün istifadə edilən şifrələmə açarı ilə uyuşmur." + "message": "Xaricə köçürülən faylın şifrəsi açılarkən xəta baş verdi. Şifrələmə açarınız, veriləri xaricə köçürmək üçün istifadə edilən şifrələmə açarı ilə uyuşmur." }, "invalidFilePassword": { "message": "Yararsız fayl parolu, lütfən xaricə köçürmə faylını yaradarkən daxil etdiyiniz parolu istifadə edin." @@ -3473,19 +3533,19 @@ "message": "Seyfi daxilə köçürməyi təsdiqlə" }, "confirmVaultImportDesc": { - "message": "Bu fayl parolla qorunur. Məlumatları daxilə köçürmək üçün fayl parolunu daxil edin." + "message": "Bu fayl parolla qorunur. Veriləri daxilə köçürmək üçün fayl parolunu daxil edin." }, "confirmFilePassword": { "message": "Fayl parolunu təsdiqlə" }, "exportSuccess": { - "message": "Seyf datası xaricə köçürüldü" + "message": "Seyf veriləri xaricə köçürüldü" }, "multifactorAuthenticationCancelled": { "message": "Çox faktorlu kimlik doğrulama ləğv edildi" }, "noLastPassDataFound": { - "message": "LastPass datası tapılmadı" + "message": "LastPass veriləri tapılmadı" }, "incorrectUsernameOrPassword": { "message": "Yanlış istifadəçi adı və ya parol" @@ -3589,10 +3649,10 @@ "message": "Problemlərin aradan qaldırılması" }, "disableHardwareAccelerationRestart": { - "message": "Avadanlıq sürətləndirməni ləğv et və yenidən başlat" + "message": "Donanım sürətləndirməni sıradan çıxart və yenidən başlat" }, "enableHardwareAccelerationRestart": { - "message": "Avadanlıq sürətləndirməni işə sal və yenidən başlat" + "message": "Donanım sürətləndirməni fəallaşdır və yenidən başlat" }, "removePasskey": { "message": "Parolu sil" @@ -3641,7 +3701,7 @@ } }, "data": { - "message": "Data" + "message": "Veri" }, "fileSends": { "message": "Fayl \"Send\"ləri" @@ -3724,7 +3784,7 @@ "message": "Bu tələb, giriş etdiyiniz remote bir cihazdan gəlir" }, "sshkeyApprovalMessageInfix": { - "message": "bura müraciət tələb edir:" + "message": "bura erişim tələb edir:" }, "sshkeyApprovalMessageSuffix": { "message": "məqsəd" @@ -3760,7 +3820,7 @@ "message": "Ekranı çəkməyə icazə ver" }, "allowScreenshotsDesc": { - "message": "Bitwarden masaüstü tətbiqinin ekran şəkillərində yaxalanmasına və remote desktop sessiyalarında görünməsinə icazə verin. Bunu sıradan çıxartsanız, bəzi xarici ekranlarda müraciət əngəllənəcək." + "message": "Bitwarden masaüstü tətbiqinin ekran şəkillərində yaxalanmasına və uzaq masaüstü seanslarında nümayiş etdirilməsinə icazə verin. Bunu sıradan çıxartsanız, bəzi xarici ekranlarda erişim əngəllənəcək." }, "confirmWindowStillVisibleTitle": { "message": "Pəncərənin hələ də göründüyünü təsdiqlə" @@ -3803,7 +3863,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "İstənilən platformada faylları və dataları hər kəslə paylaşın. İfşa olunmağı məhdudlaşdıraraq məlumatlarınız ucdan-uca şifrələnmiş qalacaq.", + "message": "İstənilən platformada faylları və veriləri hər kəslə güvənli şəkildə paylaşın. Məlumatlarınız, ifşa olunmamaq üçün ucdan-uca şifrələnmiş qalacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "generatorNudgeTitle": { @@ -3854,13 +3914,13 @@ "message": "Kimliklərinizlə, uzun qeydiyyat və ya əlaqə xanalarını daha tez avtomatik doldurun." }, "newNoteNudgeTitle": { - "message": "Həssas datalarınızı güvənli şəkildə saxlayın" + "message": "Həssas verilərinizi güvənli şəkildə saxlayın" }, "newNoteNudgeBody": { - "message": "Notlarla, bankçılıq və ya sığorta təfsilatları kimi həssas dataları təhlükəsiz saxlayın." + "message": "Notlarla, bankçılıq və ya sığorta təfsilatları kimi həssas veriləri təhlükəsiz saxlayın." }, "newSshNudgeTitle": { - "message": "Gəlişdirici dostu SSH müraciəti" + "message": "Gəlişdirici dostu SSH erişimi" }, "newSshNudgeBodyOne": { "message": "Açarlarınızı saxlayın və sürətli, şifrələnmiş kimlik doğrulama üçün SSH agentinə bağlayın.", @@ -3879,10 +3939,10 @@ "message": "Bu kolleksiyalara təyin et" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementi görə biləcək." + "message": "Yalnız bu kolleksiyalara erişimi olan təşkilat üzvləri bu elementi görə biləcək." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementləri görə biləcək." + "message": "Yalnız bu kolleksiyalara erişimi olan təşkilat üzvləri bu elementləri görə biləcək." }, "noCollectionsAssigned": { "message": "Heç bir kolleksiya təyin edilmədi" @@ -3891,7 +3951,7 @@ "message": "Təyin et" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementləri görə biləcək." + "message": "Yalnız bu kolleksiyalara erişimi olan təşkilat üzvləri bu elementləri görə biləcək." }, "bulkCollectionAssignmentWarning": { "message": "$TOTAL_COUNT$ element seçmisiniz. Düzəliş icazəniz olmadığı üçün $READONLY_COUNT$ elementi güncəlləyə bilməzsiniz.", @@ -4006,8 +4066,14 @@ } } }, + "showMore": { + "message": "Daha çox göstər" + }, + "showLess": { + "message": "Daha az göstər" + }, "enableAutotype": { - "message": "Enable Autotype" + "message": "Avto-yazmanı fəallaşdır" }, "enableAutotypeDescription": { "message": "Bitwarden, giriş yerlərini doğrulamır, qısayolu istifadə etməzdən əvvəl doğru pəncərədə və xanada olduğunuza əmin olun." diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 39c3e6abecc..8052446fadd 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Двухэтапны ўваход" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Час чакання сховішча" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Выберыце тайм-аўт для сховішча і дзеянне, якое неабходна зрабіць." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Час чакання вашага сховішча перавышае дазволеныя абмежаванні, якія прызначыла ваша арганізацыя." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Пераключыць лічыльнік сімвалаў", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Тып прылады" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "Уваход пацверджаны для $EMAIL$ на $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Вы адхілілі спробу ўваходу з іншай прылады. Калі гэта сапраўды былі вы, паспрабуйце ўвайсці з прыладай яшчэ раз." - }, "justNow": { "message": "Толькі што" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 85cb9a9ecf5..c4c1e64caea 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Двустепенно удостоверяване" }, + "vaultTimeoutHeader": { + "message": "Време за достъп до трезора" + }, "vaultTimeout": { "message": "Време за достъп" }, "vaultTimeout1": { "message": "Време за достъп" }, + "vaultTimeoutAction1": { + "message": "Действие при изтичането на времето за достъп" + }, "vaultTimeoutDesc": { "message": "Изберете колко да е времето за достъп и какво ще е действието след това." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Времето за достъп до трезора Ви превишава ограничението, определено от организацията Ви." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Изискванията на политиката за големи компании бяха приложени към настройките на времето за достъп" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Настройките на организацията Ви са задали максималното разрешено време за достъп до трезора на $HOURS$ час(а) и $MINUTES$ минута/и.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Времето за достъп превишава ограничението, зададено от Вашата организация: максимум $HOURS$ час(а) и $MINUTES$ минута/и", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Поканата е приета" }, @@ -2992,9 +3027,6 @@ "message": "Превключване на броя знаци", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Опитвате ли се да получите достъп до акаунта си?" - }, "accessAttemptBy": { "message": "Опит за достъп от $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Заявката за вписване за $EMAIL$ на $DEVICE$ е одобрена", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Вие отказахте опит за вписване от друго устройство. Ако това сте били Вие, опитайте да се впишете от устройството отново." + }, + "webApp": { + "message": "Приложение по уеб" + }, + "mobile": { + "message": "Мобилно приложение", + "description": "Mobile app" + }, + "extension": { + "message": "Добавка за браузър", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Работен плот", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Сървър" + }, + "loginRequest": { + "message": "Заявка за вписване" + }, "deviceType": { "message": "Вид устройство" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Отказване на достъпа" }, - "logInConfirmedForEmailOnDevice": { - "message": "Вписването за $EMAIL$ на $DEVICE$ е одобрено", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Вие отказахте опит за вписване от друго устройство. Ако това наистина сте били Вие, опитайте да се впишете от устройството отново." - }, "justNow": { "message": "Току-що" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Показване на повече" + }, + "showLess": { + "message": "Показване на по-малко" + }, "enableAutotype": { "message": "Включване на автоматичното въвеждане" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index da093c7b76e..229b565c533 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "দ্বি-পদক্ষেপের লগইন" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "ভল্টের সময়সীমা" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 1b2ff2d8cfa..d75193c4d2c 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Prijava u dva koraka" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vremensko ograničenje trezora" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Odaberi kada će za koliko vremena će isteći aktivnost trezora i biti izvršena odabrana radnja." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 4bfc0f97ac1..ec9c824a6b9 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Inici de sessió en dues passes" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Temps d'espera de la caixa forta" }, "vaultTimeout1": { "message": "Temps d'espera" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Trieu quan es tancarà la vostra caixa forta i feu l'acció seleccionada." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "El temps d'espera de la caixa forta supera les restriccions establertes per la vostra organització." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitació acceptada" }, @@ -2992,9 +3027,6 @@ "message": "Commuta el recompte de caràcters", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Intentes accedir al teu compte?" - }, "accessAttemptBy": { "message": "Intent d'inici de sessió per $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Tipus de dispositiu" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Denega l'accés" }, - "logInConfirmedForEmailOnDevice": { - "message": "S'ha confirmat l'inici de sessió per a $EMAIL$ a $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Heu denegat un intent d'inici de sessió des d'un altre dispositiu. Si realment heu sigut vosaltres, intenteu tornar a iniciar sessió amb el dispositiu." - }, "justNow": { "message": "Ara mateix" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index f0177478af8..54346110f5d 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Dvoufázové přihlášení" }, + "vaultTimeoutHeader": { + "message": "Časový limit trezoru" + }, "vaultTimeout": { "message": "Časový limit trezoru" }, "vaultTimeout1": { "message": "Časový limit" }, + "vaultTimeoutAction1": { + "message": "Akce vypršení časového limitu" + }, "vaultTimeoutDesc": { "message": "Vyberte, kdy vyprší bezpečnostní limit trezoru. Poté bude provedena vybraná akce." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Časový limit Vašeho trezoru překračuje omezení stanovená Vaší organizací." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Na volby časového limitu byly uplatněny požadavky podnikových zásad" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Zásady Vaší organizace nastavily maximální povolený časový limit trezoru na $HOURS$ hodin a $MINUTES$ minut.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Časový limit překračuje omezení stanovené Vaší organizací: maximálně $HOURS$ hodin a $MINUTES$ minut", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Pozvánka byla přijata" }, @@ -2992,9 +3027,6 @@ "message": "Zobrazit počet znaků", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Pokoušíte se získat přístup k Vašemu účtu?" - }, "accessAttemptBy": { "message": "Pokus o přístup z $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Požadavek na přihlášení byl schválen pro $EMAIL$ na $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Pokus o přihlášení byl zamítnut z jiného zařízení. Pokud jste to Vy, zkuste se znovu přihlásit do zařízení." + }, + "webApp": { + "message": "Webová aplikace" + }, + "mobile": { + "message": "Mobil", + "description": "Mobile app" + }, + "extension": { + "message": "Rozšíření", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Počítač", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Požadavek na přihlášení" + }, "deviceType": { "message": "Typ zařízení" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Zamítnout přístup" }, - "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í." - }, "justNow": { "message": "Právě teď" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Zobrazit více" + }, + "showLess": { + "message": "Zobrazit méně" + }, "enableAutotype": { "message": "Povolit automatický zápis" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 31359ecd1bc..a6400c36e53 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index b7051a85f77..81df0a0aeb2 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Totrins-login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Boks timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Vælg hvornår din boks skal udføre timeout-handlingen." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Din boks-timeout overskrider de organisationsbestemte restriktioner." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepteret" }, @@ -2992,9 +3027,6 @@ "message": "Vis/skjul tegnantal", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Enhedstype" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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, så prøv at logge ind med enheden igen." - }, "justNow": { "message": "Netop nu" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index c80351496b2..ec727bc69d8 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Zwei-Faktor-Authentifizierung" }, + "vaultTimeoutHeader": { + "message": "Tresor-Timeout" + }, "vaultTimeout": { "message": "Tresor-Timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout-Aktion" + }, "vaultTimeoutDesc": { "message": "Wähle, wann der Timeout deines Tresors aktiviert werden soll und welche Aktion er bewirken soll." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Dein Tresor-Timeout überschreitet die von deinem Unternehmen festgelegten Beschränkungen." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Die Unternehmens-Richtlinienanforderungen wurden auf deine Timeout-Optionen angewendet" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Deine Unternehmensrichtlinien haben das maximal zulässige Tresor-Timeout auf $HOURS$ Stunde(n) und $MINUTES$ Minute(n) festgelegt.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Das Timeout überschreitet die von deiner Organisation festgelegte Beschränkung: Maximal $HOURS$ Stunde(n) und $MINUTES$ Minute(n)", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Einladung angenommen" }, @@ -2992,9 +3027,6 @@ "message": "Zeichenanzahl ein-/ausschalten", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Versuchst du auf dein Konto zuzugreifen?" - }, "accessAttemptBy": { "message": "Zugriffsversuch von $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Anmeldeanfrage für $EMAIL$ auf $DEVICE$ genehmigt", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Du hast einen Anmeldeversuch von einem anderen Gerät abgelehnt. Wenn du das wirklich warst, versuche dich erneut mit dem Gerät anzumelden." + }, + "webApp": { + "message": "Web-App" + }, + "mobile": { + "message": "Mobile App", + "description": "Mobile app" + }, + "extension": { + "message": "Erweiterung", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Anmeldungsanfrage" + }, "deviceType": { "message": "Gerätetyp" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Zugriff ablehnen" }, - "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." - }, "justNow": { "message": "Gerade eben" }, @@ -4006,8 +4066,14 @@ } } }, + "showMore": { + "message": "Mehr anzeigen" + }, + "showLess": { + "message": "Weniger anzeigen" + }, "enableAutotype": { - "message": "Enable Autotype" + "message": "Autotype aktivieren" }, "enableAutotypeDescription": { "message": "Bitwarden überprüft die Eingabestellen nicht. Vergewissere dich, dass du dich im richtigen Fenster und Feld befindest, bevor du die Tastenkombination verwendest." diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index a6cd4fd5e15..049bfa7780b 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Σύνδεση δύο βημάτων" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Χρονικό όριο λήξης κρύπτης" }, "vaultTimeout1": { "message": "Χρονικό όριο" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Επιλέξτε πότε η κρύπτη σας θα αναλάβει τη δράση χρονικού ορίου λήξης κρύπτης." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Το χρονικό όριο του vault σας υπερβαίνει τους περιορισμούς που έχει ορίσει ο οργανισμός σας." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Η πρόσκληση έγινε αποδεκτή" }, @@ -2992,9 +3027,6 @@ "message": "Εναλλαγή αριθμού χαρακτήρων", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Προσπαθείτε να αποκτήσετε πρόσβαση στο λογαριασμό σας;" - }, "accessAttemptBy": { "message": "Προσπάθεια πρόσβασης από το $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Τύπος Συσκευής" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Άρνηση πρόσβασης" }, - "logInConfirmedForEmailOnDevice": { - "message": "Επιβεβαιώθηκε η σύνδεση του $EMAIL$ στη συσκευή $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Απορρίψατε μια προσπάθεια σύνδεσης από μια άλλη συσκευή. Αν πραγματικά ήσασταν εσείς, προσπαθήστε να συνδεθείτε ξανά με τη συσκευή." - }, "justNow": { "message": "Μόλις τώρα" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 72d40ed750f..83c021341b8 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3491,15 +3491,11 @@ "selectImportCollection": { "message": "Select a collection" }, - "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", - "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", - "placeholders": { - "destination": { - "content": "$1", - "example": "folder or collection" - } - } + "importTargetHintCollection": { + "message": "Select this option if you want the imported file contents moved to a collection" + }, + "importTargetHintFolder": { + "message": "Select this option if you want the imported file contents moved to a folder" }, "importUnassignedItemsError": { "message": "File contains unassigned items." @@ -4077,5 +4073,9 @@ }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." + }, + "moreBreadcrumbs": { + "message": "More breadcrumbs", + "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 69ba74f8650..3d4f0120eba 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organisation." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organisation policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organisation: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index b6c0369a035..4576e5212d3 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will timeout and perform the selected action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organisation policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organisation: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 3e15c2cba95..0306fd420bc 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Restarigi la dufaktoran aŭtentigon de la konto" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Ĝuste nun" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 6aed28c11f8..912d92665cd 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -42,7 +42,7 @@ "message": "Buscar en caja fuerte" }, "resetSearch": { - "message": "Reset search" + "message": "Restablecer búsqueda" }, "addItem": { "message": "Añadir elemento" @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Autenticación en dos pasos" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Tiempo de espera de la caja fuerte excedido" }, "vaultTimeout1": { "message": "Tiempo de espera" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Elige cuando se agotará el tiempo de espera de tu caja fuerte y se ejecutará la acción seleccionada." }, @@ -2130,10 +2136,10 @@ "message": "Por desgracia la integración del navegador sólo está soportada por ahora en la versión de la Mac App Store." }, "browserIntegrationWindowsStoreDesc": { - "message": "Por desgracia la integración del navegador sólo está soportada por ahora en la versión de Microsoft Store." + "message": "Lamentablemente, la integración del navegador no está actualmente soportada en la versión de Microsoft Store." }, "browserIntegrationLinuxDesc": { - "message": "Por desgracia la integración con el navegador actualmente sólo está soportada en la versión de Microsoft Store." + "message": "Lamentablemente, la integración del navegador no está actualmente soportada en la versión de linux." }, "enableBrowserIntegrationFingerprint": { "message": "Requiere verificación para la integración del navegador" @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "El tiempo de espera de tu caja fuerte excede las restricciones establecidas por tu organización." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitación aceptada" }, @@ -2992,9 +3027,6 @@ "message": "Activar recuento de caracteres", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "¿Estás intentando acceder a tu cuenta?" - }, "accessAttemptBy": { "message": "Intento de acceso de $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Tipo de dispositivo" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Denegar acceso" }, - "logInConfirmedForEmailOnDevice": { - "message": "Inicio de sesión confirmado para $EMAIL$ en $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Has denegado un intento de inicio de sesión desde otro dispositivo. Si realmente fuiste tú, intenta iniciar sesión con el dispositivo de nuevo." - }, "justNow": { "message": "Justo ahora" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 0eaf1fd7cf0..267714a3f53 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Kaheastmeline kinnitamine" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Hoidla ajalõpp" }, "vaultTimeout1": { "message": "Aegumine" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Vali millal saabub hoidla ajalõpp ning sooritatakse valitud tegevus." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Valitud hoidla ajalõpp ei ole organisatsiooni poolt määratud reeglitega kooskõlas." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Kutse vastu võetud" }, @@ -2992,9 +3027,6 @@ "message": "Loenda kirjatähtede hulka", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Seadme tüüp" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "$EMAIL$ sisselogimine seadmes $DEVICE$ on kinnitatud", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Sisselogimise päring on tühistatud. Vajadusel proovi uuesti." - }, "justNow": { "message": "Just praegu" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 4f4736ecde4..01a522a3c8e 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Bi urratseko saio hasiera" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Kutxa gotorraren itxaronaldia" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Aukeratu kutxa gotorraren itxaronaldia eta egingo den ekintza." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Zure kutxa gotorreko itxaronaldiak, zure erakundeak ezarritako murrizpenak gainditzen ditu." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index d08f4b8bd97..f48383d6aed 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "ورود دو مرحله‌ای" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "متوقف شدن گاو‌صندوق" }, "vaultTimeout1": { "message": "پایان زمان" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "انتخاب کنید که گاو‌صندوق شما چه زمانی عمل توقف زمانی گاوصندوق را انجام دهد." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "مهلت زمانی شما بیش از محدودیت‌های تعیین شده توسط سازمان‌تان است." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "دعوتنامه پذیرفته شد" }, @@ -2992,9 +3027,6 @@ "message": "تغییر تعداد کاراکترها", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "آیا در تلاش برای دسترسی به حساب کاربری خود هستید؟" - }, "accessAttemptBy": { "message": "تلاش برای دسترسی به سیستم توسط $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "نوع دستگاه" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "دسترسی را رد کن" }, - "logInConfirmedForEmailOnDevice": { - "message": "ورود به سیستم برای $EMAIL$ در $DEVICE$ تأیید شد", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "شما تلاش برای ورود به سیستم از دستگاه دیگری را رد کردید. اگر واقعاً این شما بودید، سعی کنید دوباره با دستگاه وارد شوید." - }, "justNow": { "message": "همین الان" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index fda86ee6f83..fbe43e03514 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Kaksivaiheinen kirjautuminen" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Holvin aikakatkaisu" }, "vaultTimeout1": { "message": "Aikakatkaisu" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Määritä milloin holvin aikakatkaisutoiminto suoritetaan." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Holvisi aikakatkaisu ylittää organisaatiosi asettamat rajoitukset." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Kutsu hyväksyttiin" }, @@ -2992,9 +3027,6 @@ "message": "Näytä/piilota merkkikohtainen numerointi", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Laitteen tyyppi" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "Kirjautuminen vahvistettu tunnuksella $EMAIL$ alustalla $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Estit toisen laitteen lähettämän kirjautumispyynnön. Jos kuitenkin tunnistit kirjautumisyrityksen, suorita kirjautuminen uudelleen." - }, "justNow": { "message": "Juuri nyt" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 6746f602ee6..f3f771d4004 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Dalawahang-hakbang na Pag-login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Piliin kung kailan gagawin ng iyong vault ang vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Ang iyong vault timeout ay lumalampas sa mga restriksiyon na itinakda ng iyong organisasyon." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "I-toggle ang bilang ng character", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Uri ng Device" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "Nakumpirma ang pag-login para sa $EMAIL$ sa $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Tinanggihan mo ang isang pagtatangka sa pag login mula sa ibang aparato. Kung ito talaga ay ikaw, subukang mag-log in gamit ang device." - }, "justNow": { "message": "Ngayon lamang" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 63f57c99f88..f0f2eec9c7a 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -576,7 +576,7 @@ "message": "Copier le code de vérification (TOTP)" }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Copier $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Authentification à deux facteurs" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Délai d'expiration du coffre" }, "vaultTimeout1": { "message": "Délai dépassé" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choisissez quand votre coffre réalisera l'action d'expiration." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Le délai d'expiration de votre coffre dépasse les restrictions définies par votre organisation." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation acceptée" }, @@ -2992,9 +3027,6 @@ "message": "Activer / désactiver le compte de caractères", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Essayez-vous d'accéder à votre compte ?" - }, "accessAttemptBy": { "message": "Tentative d'accès par $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Type d'appareil" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Refuser l'accès" }, - "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 à partir d'un autre appareil. Si c'était vraiment vous, essayez de vous connecter à nouveau avec l'appareil." - }, "justNow": { "message": "À l'instant" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Activer le type automatique" }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index bc21aafbba1..2e0d5b09f5c 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 55f8dc60117..116556afcb2 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "אימות דו שלבי להתחברות" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "סגירת כספת אוטומטית" }, "vaultTimeout1": { "message": "פסק זמן" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "בחר כמה זמן יעבור כדי שהכספת תסגר לאחר חוסר פעילות ותבצע את הפעולה שנבחרה." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "פסק הזמן לכספת שלך חורג מהמגבלות שנקבעו על ידי הארגון שלך." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "ההזמנה התקבלה" }, @@ -2992,9 +3027,6 @@ "message": "החלף מצב מונה תווים", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "האם אתה מנסה לגשת לחשבון שלך?" - }, "accessAttemptBy": { "message": "ניסיון גישה על ידי $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "סוג מכשיר" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "דחה גישה" }, - "logInConfirmedForEmailOnDevice": { - "message": "הכניסה אושרה עבור $EMAIL$ ב־$DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "דחית ניסיון כניסה ממכשיר אחר. אם זה באמת היית אתה, נסה להיכנס עם המכשיר שוב." - }, "justNow": { "message": "זה עתה" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 5c549ac443c..234d1ca9283 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 1487d815c63..5266c7cb137 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Prijava dvostrukom autentifikacijom" }, + "vaultTimeoutHeader": { + "message": "Istek trezora" + }, "vaultTimeout": { "message": "Istek trezora" }, "vaultTimeout1": { "message": "Vrijeme isteka" }, + "vaultTimeoutAction1": { + "message": "Radnja nakon isteka" + }, "vaultTimeoutDesc": { "message": "Odaberi kada će isteći trezor i koja će se radnja izvršiti." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Vrijeme isteka premašuje ograničenje koju je postavila tvoja organizacija." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Pravila tvrtke primijenjena su na vrijeme isteka" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Pravilo tvoje organizacije utječe na istek trezora. Najveće dozvoljeno vrijeme isteka je $HOURS$:$MINUTES$ h.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Tvoja organizacija je zadano postavila kraće vrijeme isteka. Najviše: $HOURS$:$MINUTES$", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Pozivnica prihvaćena" }, @@ -2992,9 +3027,6 @@ "message": "Prikaži/Sakrij broj znakova", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Pokušavaš li pristupiti svom računu?" - }, "accessAttemptBy": { "message": "$EMAIL$ pokušava pristupiti", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Prijava za $EMAIL$ potvrđena na uređaju $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Odbijena je prijava na drugom uređaju. Ako si ovo stvarno ti, pokušaj se ponovno prijaviti uređajem." + }, + "webApp": { + "message": "Web aplikacija" + }, + "mobile": { + "message": "Mobitel", + "description": "Mobile app" + }, + "extension": { + "message": "Proširenje", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Poslužitelj" + }, + "loginRequest": { + "message": "Zahtjev za prijavu" + }, "deviceType": { "message": "Vrsta uređaja" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Odbij pristup" }, - "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." - }, "justNow": { "message": "Upravo" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Prikaži više" + }, + "showLess": { + "message": "Pokaži manje" + }, "enableAutotype": { "message": "Omogući automatski unos" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 8bbceea67b2..2f0d15b3fe0 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Kétlépcsős bejelentkezés" }, + "vaultTimeoutHeader": { + "message": "Széf időkifutás" + }, "vaultTimeout": { "message": "Széf időkifutás" }, "vaultTimeout1": { "message": "Időkifutás" }, + "vaultTimeoutAction1": { + "message": "Időkifutási művelet" + }, "vaultTimeoutDesc": { "message": "Válasszuk ki, hogy a széfnél mikor legyen időkifutás és a kiválasztott művelet végrehajtása." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "A széf időkorlátja túllépi a szervezet által beállított korlátozást." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "A vállalkozáspolitikai követelményeket alkalmazra kerültek az időkifutási opciókra.." + }, + "vaultTimeoutPolicyInEffect": { + "message": "A szervezeti szabályzata $HOURS$ órára és $MINUTES$ percre állította be a maximálisan megengedett széf időkifutást.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Az időkifutás meghaladja a szervezet által beállított korlátozást: $HOURS$ óra és $MINUTES$ perc maximum.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "A meghívás elfogadásra került." }, @@ -2992,9 +3027,6 @@ "message": "Karakterszámláló váltás", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "A fiókhoz próbálunk hozzáférni?" - }, "accessAttemptBy": { "message": "Bejelentkezési kísérlet $EMAIL$ segítségével", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "A bejelentkezési kérelem jóváhagyásra került: $EMAIL$ - $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "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." + }, + "webApp": { + "message": "Webalkalmazás" + }, + "mobile": { + "message": "Mobil", + "description": "Mobile app" + }, + "extension": { + "message": "Kiterjesztés", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Asztali", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Szerver" + }, + "loginRequest": { + "message": "Bejelentkezés kérés" + }, "deviceType": { "message": "Eszköz típus" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Hozzáférés megtagadása" }, - "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." - }, "justNow": { "message": "Éppen most" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Több megjelenítése" + }, + "showLess": { + "message": "Kevesebb megjelenítése" + }, "enableAutotype": { "message": "Autotípus engedélyezése" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 045f8d16050..01a10a65ef4 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Info masuk dua langkah" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Batas Waktu Brankas" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Pilih kapan brankas Anda akan timeout dan melakukan tindakan yang dipilih." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Waktu tunggu brankas Anda melebihi batasan yang diatur organisasi Anda." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 36f52167fcf..42eafea9717 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -42,7 +42,7 @@ "message": "Cerca nella cassaforte" }, "resetSearch": { - "message": "Reset search" + "message": "Svuota ricerca" }, "addItem": { "message": "Aggiungi elemento" @@ -576,7 +576,7 @@ "message": "Copia codice di verifica (TOTP)" }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Copia $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Verifica in due passaggi" }, + "vaultTimeoutHeader": { + "message": "Timeout cassaforte" + }, "vaultTimeout": { "message": "Timeout cassaforte" }, "vaultTimeout1": { "message": "Tempo limite" }, + "vaultTimeoutAction1": { + "message": "Azione al timeout" + }, "vaultTimeoutDesc": { "message": "Scegli quando la tua cassaforte eseguirà l'azione di timeout." }, @@ -1443,7 +1449,7 @@ "description": "Copy credit card security code (CVV)" }, "cardNumber": { - "message": "card number" + "message": "numero carta" }, "premiumMembership": { "message": "Abbonamento Premium" @@ -2401,7 +2407,7 @@ "message": "Questa azione è protetta. Inserisci la tua password principale di nuovo per verificare la tua identità." }, "masterPasswordSuccessfullySet": { - "message": "Master password successfully set" + "message": "Password principale impostata correttamente" }, "updatedMasterPassword": { "message": "Password principale aggiornata" @@ -2416,13 +2422,13 @@ "message": "La tua password principale non soddisfa uno o più politiche della tua organizzazione. Per accedere alla cassaforte, aggiornala ora. Procedere ti farà uscire dalla sessione corrente, richiedendoti di accedere di nuovo. Le sessioni attive su altri dispositivi potrebbero continuare a rimanere attive per un massimo di un'ora." }, "changePasswordWarning": { - "message": "After changing your password, you will need to log in with your new password. Active sessions on other devices will be logged out within one hour." + "message": "Dopo aver cambiato la password, dovrai accedere con quella nuova. Le sessioni attive su altri dispositivi saranno disconnesse entro un'ora." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Change your master password to complete account recovery." + "message": "Cambia la password principale per completare il recupero dell'account." }, "updateMasterPasswordSubtitle": { - "message": "Your master password does not meet this organization’s requirements. Change your master password to continue." + "message": "La tua password principale non soddisfa i requisiti dell'organizzazione. Aggiornala per continuare." }, "tdeDisabledMasterPasswordRequired": { "message": "La tua organizzazione ha disabilitato la crittografia affidabile del dispositivo. Per favore imposta una password principale per accedere alla tua cassaforte." @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Il timeout della tua cassaforte supera i limiti impostati dalla tua organizzazione." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "I requisiti di politica aziendale sono stati applicati alle opzioni di timeout" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Le politiche dell'organizzazione hanno impostato il timeout massimo consentito della tua cassaforte su $HOURS$ ore e $MINUTES$ minuti.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Il timeout supera la restrizione impostata dalla tua organizzazione: massimo $HOURS$ ora/e e $MINUTES$ minuto/i", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invito accettato" }, @@ -2992,9 +3027,6 @@ "message": "Attiva/disattiva conteggio caratteri", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Stai cercando di accedere al tuo account?" - }, "accessAttemptBy": { "message": "Tentativo di accesso di $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Richiesta di accesso approvata per $EMAIL$ su $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Hai negato un tentativo di accesso da un altro dispositivo. Se eri davvero tu, prova di nuovo ad accedere con il dispositivo." + }, + "webApp": { + "message": "Bitwarden Web" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Estensione", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "Linea di comando" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Richiesta di accesso" + }, "deviceType": { "message": "Tipo di dispositivo" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Nega accesso" }, - "logInConfirmedForEmailOnDevice": { - "message": "Login per $EMAIL$ da $DEVICE$ confermato", - "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 ad accedere con il dispositivo di nuovo." - }, "justNow": { "message": "Adesso" }, @@ -3162,10 +3222,10 @@ "message": "Richiedi approvazione dell'amministratore" }, "unableToCompleteLogin": { - "message": "Unable to complete login" + "message": "Impossibile completare l'accesso" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { - "message": "You need to log in on a trusted device or ask your administrator to assign you a password." + "message": "È necessario accedere da un dispositivo attendibile oppure chiedere all'amministratore l'assegnazione di una password." }, "region": { "message": "Regione" @@ -3559,27 +3619,27 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "message": "La corrispondenza URL è il metodo predefinito per identificare i suggerimenti di riempimento automatico.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { - "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "message": "'Espressione regolare' è un'opzione avanzata con un maggior rischio di esporre le credenziali.", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { - "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "message": "'Inizia con' è un'opzione con un maggior rischio di esporre le credenziali.", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "More about match detection", + "message": "Ulteriori informazioni sulla corrispondenza degli URL", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { - "message": "Advanced options", + "message": "Opzioni avanzate", "description": "Advanced option placeholder for uri option component" }, "warningCapitalized": { - "message": "Warning", + "message": "Attenzione", "description": "Warning (should maintain locale-relevant capitalization)" }, "success": { @@ -4006,10 +4066,16 @@ } } }, + "showMore": { + "message": "Mostra di più" + }, + "showLess": { + "message": "Mostra di meno" + }, "enableAutotype": { - "message": "Enable Autotype" + "message": "Abilita auto immissione" }, "enableAutotypeDescription": { - "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." + "message": "Bitwarden non convalida i campi di input: assicurati di essere nella finestra e nel campo di testo corretti prima di usare la scorciatoia." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index e09ac5629ed..0ac3473f17f 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "2段階認証" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "保管庫のタイムアウト" }, "vaultTimeout1": { "message": "タイムアウト" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "保管庫がタイムアウトし、選択したアクションを実行するタイミングを選択します。" }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "保管庫のタイムアウトが組織によって設定された制限を超えています。" }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "招待が承認されました" }, @@ -2992,9 +3027,6 @@ "message": "文字カウントを切り替える", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "アカウントにアクセスしようとしていますか?" - }, "accessAttemptBy": { "message": "$EMAIL$ によるログインの試行", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "デバイス種別" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "アクセスを拒否" }, - "logInConfirmedForEmailOnDevice": { - "message": "$EMAIL$ に $DEVICE$ でのログインを承認しました", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "別のデバイスからのログイン試行を拒否しました。本当にあなたであった場合は、もう一度デバイスでログインしてみてください。" - }, "justNow": { "message": "たった今" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 7bfd1e5eda6..5b21fe1ae07 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "მოლოდინის ვადა" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "მოწყობილობის ტიპი" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "ახლა" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index bc21aafbba1..2e0d5b09f5c 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 912e7d00360..ac1cc7b451d 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "ಎರಡು ಹಂತದ ಲಾಗಿನ್" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "ವಾಲ್ಟ್ ಕಾಲಾವಧಿ" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "ನಿಮ್ಮ ಕಮಾನು ಸಮಯ ಮೀರಲಿ ಮತ್ತು ಆಯ್ದ ಕ್ರಮವನ್ನು ನಿರ್ವಹಿಸುವಾಗ ಆರಿಸಿಕೊಳ್ಳಿ." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 0616fda6a0c..4d1f6709e4a 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "2단계 인증" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "보관함 시간 제한" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "보관함이 언제까지 시간을 제한하고 선택된 행동을 수행하지 선택해주세요." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "볼트 제한 시간이 조직에서 설정한 제한을 초과합니다." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "글자 수 표시하기", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "기기 유형" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "$DEVICE$에서 $EMAIL$(으)로의 로그인이 확인되었습니다", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "다른 기기에서 로그인 시도를 거부했습니다. 본인이시라면, 기기로 로그인을 다시 시도하세요." - }, "justNow": { "message": "방금 전" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 87cff181fd3..01e92251420 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Dviejų žingsnių prisijungimas" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Atsijungta nuo saugyklos" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Pasirinkite kada jūsų saugykla įvykdys laiko limito veiksmą." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Jūsų saugyklos skirtasis laikas viršija jūsų organizacijos nustatytus apribojimus." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Perjungti simbolių skaičių", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Įrenginio tipas" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "$EMAIL$ prisijungimas patvirtinas $DEVICE$ įrenginyje", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Jūs neleidote prisijungti iš kito įrenginio. Jei tai tikrai buvote jūs, bandykite vėl prisijungti iš įrenginio." - }, "justNow": { "message": "Ką tik" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index e0fddc333b4..63f03bf5dfd 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -79,7 +79,7 @@ "message": "Pielikumi" }, "viewItem": { - "message": "Skatīt vienumu" + "message": "Apskatīt vienumu" }, "name": { "message": "Nosaukums" @@ -573,7 +573,7 @@ "message": "Ievietot vietrādi starpliktuvē" }, "copyVerificationCodeTotp": { - "message": "Ievietot Apliecinājuma kodu (TOTP) starpliktuvē" + "message": "Ievietot apliecinājuma kodu (TOTP) starpliktuvē" }, "copyFieldCipherName": { "message": "Ievietot starpliktuvē $FIELD$, $CIPHERNAME$", @@ -955,10 +955,10 @@ "message": "Ievieto savu drošības atslēgu datora USB ligzdā! Ja tai ir poga, pieskaries tai!" }, "recoveryCodeDesc": { - "message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopšanas kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!" + "message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopes kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!" }, "recoveryCodeTitle": { - "message": "Atgūšanas kods" + "message": "Atkopes kods" }, "authenticatorAppTitle": { "message": "Autentificētāja lietotne" @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Divpakāpju pieteikšanās" }, + "vaultTimeoutHeader": { + "message": "Glabātavas noildze" + }, "vaultTimeout": { "message": "Glabātavas noildze" }, "vaultTimeout1": { "message": "Noildze" }, + "vaultTimeoutAction1": { + "message": "Noildzes darbība" + }, "vaultTimeoutDesc": { "message": "Izvēlēties, kad glabātavai iestāsies noildze un tiks izpildīta atlasītā darbība." }, @@ -2419,7 +2425,7 @@ "message": "Pēc savas paroles nomainīšanas būs nepieciešams pieteikties ar jauno paroli. Spēkā esošajās sesijās citās ierīcēs stundas laikā notiks atteikšanās." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Jānomaina sava galvenā'parole, lai pabeigtu konta atkopi." + "message": "Jānomaina sava galvenā parole, lai pabeigtu konta atkopi." }, "updateMasterPasswordSubtitle": { "message": "Galvenā parole neatbilst šīs apvienības prasībām. Jānomaina sava galvenā parole, lai turpinātu." @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Glabātavas noildze pārsniedz apvienības uzstādītos ierobežojumus." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Noildzes iespējām tika piemērotas uzņēmējdarbības nosacījumu prasības" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Apvienības pamatnostādnēs ir iestatīta lielākā pieļaujamā glabātavas noildze: $HOURS$ stunda(s) un $MINUTES$ minūte(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Noildze pārsniedz apvienības iestatīto ierobežojumu: ne vairāk kā $HOURS$ stunda(s) un $MINUTES$ minūte(s)", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Uzaicinājums apstiprināts" }, @@ -2980,10 +3015,10 @@ "message": "Ir jāuzstāda pieteikšanās ar ierīci Bitwarden lietotnes iestatījumos. Nepieciešama cita iespēja?" }, "viewAllLogInOptions": { - "message": "Skatīt visas pieteikšanās iespējas" + "message": "Apskatīt visas pieteikšanās iespējas" }, "viewAllLoginOptions": { - "message": "Skatīt visas pieteikšanās iespējas" + "message": "Apskatīt visas pieteikšanās iespējas" }, "resendNotification": { "message": "Atkārtoti nosūtīt paziņojumu" @@ -2992,9 +3027,6 @@ "message": "Pārslēgt rakstzīmju skaita attēlošanu", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Vai mēģini piekļūt savam kontam?" - }, "accessAttemptBy": { "message": "$EMAIL$ piekļuves mēģinājums", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "$EMAIL$ pieteikšanās pieprasījums apstiprināts $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Tu noraidīji pieteikšanās mēģinājumu no citas ierīces. Ja tas biji Tu, mēģini pieteikties no ierīces vēlreiz!" + }, + "webApp": { + "message": "Tīmekļa lietotne" + }, + "mobile": { + "message": "Tālrunis", + "description": "Mobile app" + }, + "extension": { + "message": "Paplašinājums", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Darbvirsma", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Serveris" + }, + "loginRequest": { + "message": "Pieteikšanās pieprasījums" + }, "deviceType": { "message": "Ierīces veids" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Noraidīt piekļuvi" }, - "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!" - }, "justNow": { "message": "Tikko" }, @@ -3181,7 +3241,7 @@ "message": "pašmitināts" }, "accessDenied": { - "message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai skatītu šo lapu." + "message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai apskatītu šo lapu." }, "accountSuccessfullyCreated": { "message": "Konts ir veiksmīgi izveidots." @@ -3567,7 +3627,7 @@ "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { - "message": "\"Sākas ar' ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.", + "message": "“Sākas ar” ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { @@ -3607,7 +3667,7 @@ "message": "Kļūda mērķa mapes piešķiršanā." }, "viewItemsIn": { - "message": "Skatīt $NAME$ vienumus", + "message": "Apskatīt $NAME$ vienumus", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -3760,7 +3820,7 @@ "message": "Atļaut ekrāna tveršanu" }, "allowScreenshotsDesc": { - "message": "Ļaut Bitwarden darbvirsmas lietotni tvert ekrānuzņēmumos un rādīt attālās darbvirsmas sesijās. Atspējošana liegs piekļuvu atsevišķos ārējos ekrānos." + "message": "Ļaut Bitwarden darbvirsmas lietotni tvert ekrānuzņēmumos un rādīt attālās darbvirsmas sesijās. Atspējošana liegs piekļuvi atsevišķos ārējos ekrānos." }, "confirmWindowStillVisibleTitle": { "message": "Apstirpināt, ka logs joprojām ir redzams" @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Rādīt vairāk" + }, + "showLess": { + "message": "Rādīt mazāk" + }, "enableAutotype": { "message": "Iespējot automātisko ievadi" }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 2fe15dfc5ab..fe818806bbf 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Prijava u dva koraka" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 6dae8440411..ccee87678fb 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "രണ്ട്-ഘട്ട പ്രവേശനം" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "വാൾട് ടൈംഔട്ട്" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "തങ്ങളുടെ വാൾട് എപ്പോൾ ടൈംഔട്ട് ആകും എന്ന് നിശ്ചയിക്കുക. തിരഞ്ഞെടുത്ത പ്രവർത്തനം നടത്തുക." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index bc21aafbba1..2e0d5b09f5c 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 61ad8e5b8ad..2c0b5311b29 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 2ef5c275d45..bcf58d8f243 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "2-trinnsinnlogging" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Tidsavbrudd i hvelvet" }, "vaultTimeout1": { "message": "Tidsavbrudd" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Velg når hvelvet ditt skal tidsavbrytes og utføre den valgte handlingen." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Tidsavbruddet ditt for hvelvet overstiger begrensningene som er satt av organisasjonen din." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitasjon akseptert" }, @@ -2992,9 +3027,6 @@ "message": "Vis/skjul antall tegn", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Enhetstype" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Nekt tilgang" }, - "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." - }, "justNow": { "message": "Akkurat nå" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 701d7c5fc26..e08ed353978 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index ed0c22a2165..1d4d930a67e 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Tweestapsaanmelding" }, + "vaultTimeoutHeader": { + "message": "Time-out van de kluis" + }, "vaultTimeout": { "message": "Time-out van de kluis" }, "vaultTimeout1": { "message": "Time-out" }, + "vaultTimeoutAction1": { + "message": "Time-out actie" + }, "vaultTimeoutDesc": { "message": "Stel de time-out van de kluis en de bijbehorende actie in." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Je kluis time-out is hoger dan het maximum van jouw organisatie." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Bedrijfsbeleidseisen zijn toegepast op je time-out instellingen" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Het beleid van je organisatie heeft invloed op de time-out van je kluis. De maximaal toegestane kluis time-out is $HOURS$ uur en $MINUTES$ minuten.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Time-out overschrijdt de beperking van je organisatie: $HOURS$ uur en $MINUTES$ minu(u) t(en) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Uitnodiging geaccepteerd" }, @@ -2992,9 +3027,6 @@ "message": "Tekentelling in-/uitschakelen", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Probeer je toegang te krijgen tot je account?" - }, "accessAttemptBy": { "message": "Inlogpoging door $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Inloggen voor $EMAIL$ goedgekeurd op $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "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." + }, + "webApp": { + "message": "Web-app" + }, + "mobile": { + "message": "Mobiel", + "description": "Mobile app" + }, + "extension": { + "message": "Extensie", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Log-inverzoek" + }, "deviceType": { "message": "Apparaattype" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Toegang weigeren" }, - "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." - }, "justNow": { "message": "Zojuist" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Meer weergeven" + }, + "showLess": { + "message": "Minder weergeven" + }, "enableAutotype": { "message": "Autotypen inschakelen" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 64c862e6867..5a45d379f9d 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Tidsavbrot for kvelv" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index a941226ac59..172e0079201 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 3342250e596..55093da1245 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Logowanie dwustopniowe" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Blokowanie sejfu" }, "vaultTimeout1": { "message": "Limit czasu" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Wybierz, kiedy sejf zostanie zablokowany i wykonaj następującą akcję." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Czas blokowania sejfu przekracza limit określony przez organizację." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Zaproszenie zostało zaakceptowane" }, @@ -2992,9 +3027,6 @@ "message": "Pokaż / Ukryj licznik znaków", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Czy próbujesz uzyskać dostęp do swojego konta?" - }, "accessAttemptBy": { "message": "Próba dostępu przez $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Typ urządenia" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Odmów dostępu" }, - "logInConfirmedForEmailOnDevice": { - "message": "Logowanie potwierdzone dla $EMAIL$ dnia $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." - }, "justNow": { "message": "Teraz" }, @@ -3111,7 +3171,7 @@ "message": "Ważne:" }, "accessing": { - "message": "Uzyskiwanie dostępu" + "message": "Serwer" }, "accessTokenUnableToBeDecrypted": { "message": "Zostałeś wylogowany, ponieważ token dostępu nie mógł zostać odszyfrowany. Zaloguj się ponownie, aby rozwiązać ten problem." @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 05f8e0ba776..84236796650 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Autenticação em duas etapas" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Tempo Limite do Cofre" }, "vaultTimeout1": { "message": "Tempo esgotado" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Escolha quando o tempo limite do seu cofre irá se esgotar e execute a ação selecionada." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "O tempo limite do seu cofre excede as restrições definidas por sua organização." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Convite aceito" }, @@ -2992,9 +3027,6 @@ "message": "Ativar/Desativar contagem de caracteres", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Você está tentando acessar sua conta?" - }, "accessAttemptBy": { "message": "Tentativa de acesso por $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Tipo de dispositivo" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Negar acesso" }, - "logInConfirmedForEmailOnDevice": { - "message": "Acesso confirmado para $EMAIL$ em $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Você negou uma tentativa de acesso de outro dispositivo. Se isso realmente foi você, tente fazer login com o dispositivo novamente." - }, "justNow": { "message": "Agora há pouco" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Habilitar Autotype" }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 598e4ad4c8f..3f1fb3e5039 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Verificação de dois passos" }, + "vaultTimeoutHeader": { + "message": "Tempo limite do cofre" + }, "vaultTimeout": { "message": "Tempo limite do cofre" }, "vaultTimeout1": { "message": "Tempo limite" }, + "vaultTimeoutAction1": { + "message": "Ação de tempo limite" + }, "vaultTimeoutDesc": { "message": "Escolha quando é que o seu cofre executará a ação de tempo limite do cofre." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "O tempo limite do seu cofre excede as restrições definidas pela sua organização." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Os requisitos da política empresarial foram aplicados às suas opções de tempo limite" + }, + "vaultTimeoutPolicyInEffect": { + "message": "As políticas da sua organização definiram o tempo limite máximo permitido do cofre de $HOURS$ hora(s) e $MINUTES$ minuto(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "O tempo limite excede a restrição definida pela sua organização: $HOURS$ hora(s) e $MINUTES$ minuto(s) no máximo", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Convite aceite" }, @@ -2992,9 +3027,6 @@ "message": "Mostrar/ocultar contagem de carateres", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Está a tentar aceder à sua conta?" - }, "accessAttemptBy": { "message": "Tentativa de acesso por $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Pedido de início de sessão aprovado para $EMAIL$ no $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "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." + }, + "webApp": { + "message": "Aplicação web" + }, + "mobile": { + "message": "Móvel", + "description": "Mobile app" + }, + "extension": { + "message": "Extensão", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Computador", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Servidor" + }, + "loginRequest": { + "message": "Pedido de início de sessão" + }, "deviceType": { "message": "Tipo de dispositivo" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Recusar acesso" }, - "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." - }, "justNow": { "message": "Agora mesmo" }, @@ -3181,7 +3241,7 @@ "message": "auto-hospedado" }, "accessDenied": { - "message": "Acesso negado. Não tem permissão para visualizar esta página." + "message": "Acesso negado. Não tem permissão para ver esta página." }, "accountSuccessfullyCreated": { "message": "Conta criada com sucesso!" @@ -3563,7 +3623,7 @@ "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { - "message": "A \"expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.", + "message": "A \"Expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { @@ -4006,8 +4066,14 @@ } } }, + "showMore": { + "message": "Mostrar mais" + }, + "showLess": { + "message": "Mostrar menos" + }, "enableAutotype": { - "message": "Ativar introdução automática" + "message": "Ativar digitação automática" }, "enableAutotypeDescription": { "message": "O Bitwarden não valida a introdução de localizações. Certifique-se de que está na janela e no campo corretos antes de utilizar o atalho." diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 67d219eb56b..03064f271d4 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Autentificare în două etape" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Timp de expirare seif" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Alegeți când seiful dvs. va efectua acțiunea de expirare a seifului." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Timpul de expirare al seifului depășește restricțiile stabilite de organizația dvs." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 5453ab0192d..cd8179edc64 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Двухэтапная аутентификация" }, + "vaultTimeoutHeader": { + "message": "Тайм-аут хранилища" + }, "vaultTimeout": { "message": "Тайм-аут хранилища" }, "vaultTimeout1": { "message": "Тайм-аут" }, + "vaultTimeoutAction1": { + "message": "Тайм-аут действия" + }, "vaultTimeoutDesc": { "message": "Выберите тайм-аут для хранилища и действие, которое необходимо предпринять." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Тайм-аут вашего хранилища превышает ограничения, установленные вашей организацией." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "К настройкам тайм-аута были применены требования корпоративной политики" + }, + "vaultTimeoutPolicyInEffect": { + "message": "В соответствии с политиками вашей организации максимально допустимый тайм-аут хранилища составляет $HOURS$ час. и $MINUTES$ мин.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Время ожидания превышает установленное организацией максимальное ограничение: $HOURS$ час. $MINUTES$ мин.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Приглашение принято" }, @@ -2992,9 +3027,6 @@ "message": "Показать количество символов", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Вы пытаетесь получить доступ к своему аккаунту?" - }, "accessAttemptBy": { "message": "Попытка доступа $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Запрос входа для $EMAIL$ на $DEVICE$ одобрен", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Вы отклонили попытку авторизации с другого устройства. Если это были вы, попробуйте авторизоваться с этого устройства еще раз." + }, + "webApp": { + "message": "Веб-приложение" + }, + "mobile": { + "message": "Мобильный", + "description": "Mobile app" + }, + "extension": { + "message": "Расширение", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Компьютер", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Сервер" + }, + "loginRequest": { + "message": "Запрос на вход" + }, "deviceType": { "message": "Тип устройства" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Отказать в доступе" }, - "logInConfirmedForEmailOnDevice": { - "message": "Вход подтвержден для $EMAIL$ на $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Вы отклонили попытку авторизации с другого устройства. Если это действительно были вы, попробуйте авторизоваться с этого устройства еще раз." - }, "justNow": { "message": "Только что" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Больше" + }, + "showLess": { + "message": "Меньше" + }, "enableAutotype": { "message": "Включить автоввод" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index c2c764c0cd1..6c8dfd84d0f 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index af16a68f3e0..ea1f291ca3e 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Dvojstupňové prihlásenie" }, + "vaultTimeoutHeader": { + "message": "Časový limit pre trezor" + }, "vaultTimeout": { "message": "Časový limit pre trezor" }, "vaultTimeout1": { "message": "Časový limit" }, + "vaultTimeoutAction1": { + "message": "Akcia pri vypršaní časového limitu" + }, "vaultTimeoutDesc": { "message": "Vyberte, kedy vyprší časový limit trezora a vykoná sa zvolená akcia." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Časový limit vášho trezora prekračuje obmedzenia nastavené vašou organizáciou." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Na možnosti pri vypršaní časového limitu boli uplatnené požiadavky pravidiel spoločnosti" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Pravidlá organizácie nastavili maximálny povolený časový limit trezoru na $HOURS$ hod. a $MINUTES$ min.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Časový limit prekračuje obmedzenie nastavené vašou organizáciou: maximálne $HOURS$ hod. a $MINUTES$ min", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Pozvánka prijatá" }, @@ -2992,9 +3027,6 @@ "message": "Prepnúť počítadlo znakov", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Snažíte sa získať prístup k svojmu účtu?" - }, "accessAttemptBy": { "message": "Pokus o prístup z $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Potvrdené prihlásenie pre $EMAIL$ na $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Odmietli ste pokus o prihlásenie z iného zariadenia. Ak ste to boli vy, skúste sa prihlásiť pomocou zariadenia znova." + }, + "webApp": { + "message": "Webová aplikácia" + }, + "mobile": { + "message": "Mobil", + "description": "Mobile app" + }, + "extension": { + "message": "Rozšírenie", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Počítač", + "description": "Desktop app" + }, + "cli": { + "message": "Príkazový riadok (CLI)" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Žiadosť o prihlásenie" + }, "deviceType": { "message": "Typ zariadenia" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Zamietnuť prístup" }, - "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." - }, "justNow": { "message": "Práve teraz" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Zobraziť viac" + }, + "showLess": { + "message": "Zobraziť menej" + }, "enableAutotype": { "message": "Povoliť automatické vpisovanie" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index cd4fc44da27..c96874d50d2 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Prijava v dveh korakih" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Časovna omejitev trezorja" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 805663e1a81..c8b07a79cc8 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Дво-коракна лозинка" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Тајмаут сефа" }, "vaultTimeout1": { "message": "Време трајања" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Изаберите када ће сеф истећи и извршити одабрану радњу." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Време истека вашег сефа је премашило дозвољена ограничења од стране ваше организације." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Позив прихваћен" }, @@ -2992,9 +3027,6 @@ "message": "Пребаци бројање слова", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Да ли покушавате да приступите вашем налогу?" - }, "accessAttemptBy": { "message": "Покушај приступа са $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Тип уређаја" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Одбити приступ" }, - "logInConfirmedForEmailOnDevice": { - "message": "Пријава потврђена за $EMAIL$ на $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Одбили сте покушај пријаве са другог уређаја. Ако сте то заиста били ви, покушајте поново да се пријавите помоћу уређаја." - }, "justNow": { "message": "Управо сада" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Упали ауто-унос" }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 6b560cdec5b..0c20d8ba2b5 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Tvåfaktorsautentisering" }, + "vaultTimeoutHeader": { + "message": "Timeout för valv" + }, "vaultTimeout": { "message": "Valvets tidsgräns" }, "vaultTimeout1": { "message": "Tidsgräns" }, + "vaultTimeoutAction1": { + "message": "Åtgärd vid timeout" + }, "vaultTimeoutDesc": { "message": "Välj när valvets tidsgräns överskrids och den valda åtgärden utförs." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Ditt valvs tid för timeout överskrider de begränsningar som fastställts av din organisation." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Företagets policykrav har tillämpats på dina tidsgränsalternativ" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Din organisations policy har fastställt den maximalt tillåtna tidsgränsen för valvet till $HOURS$ timmar och $MINUTES$ minuter.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Tidsgränsen överskrider den begränsning som din organisation har ställt in: $HOURS$ timmar och $MINUTES$ minut(er) maximalt", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Inbjudan accepterad" }, @@ -2992,9 +3027,6 @@ "message": "Växla teckenantal", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Försöker du komma åt ditt konto?" - }, "accessAttemptBy": { "message": "Åtkomstförsök via $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Inloggningsbegäran godkänd för $EMAIL$ på $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Du nekade ett inloggningsförsök från en annan enhet. Om detta var du, prova att logga in med den enheten igen." + }, + "webApp": { + "message": "Webbapp" + }, + "mobile": { + "message": "Mobil", + "description": "Mobile app" + }, + "extension": { + "message": "Tillägg", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Inloggningsbegäran" + }, "deviceType": { "message": "Enhetstyp" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Neka åtkomst" }, - "logInConfirmedForEmailOnDevice": { - "message": "Inloggning bekräftad för $EMAIL$ på $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Du nekade ett inloggningsförsök från en annan enhet. Om detta verkligen var du, försök att logga in med enheten igen." - }, "justNow": { "message": "Just nu" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Visa mer" + }, + "showLess": { + "message": "Visa mindre" + }, "enableAutotype": { "message": "Aktivera automatisk inmatning" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index bc21aafbba1..2e0d5b09f5c 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Two-step login" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "Vault timeout" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index da9ad03a3f7..4243cba8e42 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "เข้าสู่ระบบแบบสองขั้นตอน" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "หมดเวลาล็อกตู้เซฟ" }, "vaultTimeout1": { "message": "Timeout" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Invitation accepted" }, @@ -2992,9 +3027,6 @@ "message": "Toggle character count", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "Device Type" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "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." - }, "justNow": { "message": "Just now" }, @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 4beacd6dbb1..7a81a0642fc 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -277,7 +277,7 @@ "message": "Şifre çözme sorunu" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden, aşağıda listelenen kasa öğelerinin şifresini çözemedi." }, "contactCSToAvoidDataLossPart1": { "message": "Contact customer success", @@ -809,7 +809,7 @@ "message": "Ana parola ipucu" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Parola gücü puanı: $SCORE$", "placeholders": { "score": { "content": "$1", @@ -942,14 +942,14 @@ } }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "Bu cihazda 30 gün boyunca sorma" }, "selectAnotherMethod": { "message": "Başka bir yöntem seç", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "Kurtarma kodu kullan" }, "insertU2f": { "message": "Güvenlik anahtarınızı bilgisayarınızın USB portuna takın. Bir düğmesi varsa dokunun." @@ -1015,7 +1015,7 @@ "message": "İki aşamalı giriş seçenekleri" }, "selectTwoStepLoginMethod": { - "message": "Select two-step login method" + "message": "İki aşamalı giriş yöntemini seçin" }, "selfHostedEnvironment": { "message": "Şirket içinde barındırılan ortam" @@ -1024,7 +1024,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": "İleri düzey yapılandırma için her hizmetin taban URL'sini bağımsız olarak belirleyebilirsiniz." }, "selfHostedEnvFormInvalid": { "message": "You must add either the base Server URL or at least one custom environment." @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "İki aşamalı giriş" }, + "vaultTimeoutHeader": { + "message": "Kasa zaman aşımı" + }, "vaultTimeout": { "message": "Kasa zaman aşımı" }, "vaultTimeout1": { "message": "Zaman aşımı" }, + "vaultTimeoutAction1": { + "message": "Zaman aşımı eylemi" + }, "vaultTimeoutDesc": { "message": "Kasa zaman aşımı eyleminin ne zaman gerçekleştirileceğini seçin." }, @@ -1853,10 +1859,10 @@ "message": "Hesabınızı silmek kalıcıdır. Geri alınamaz." }, "cannotDeleteAccount": { - "message": "Cannot delete account" + "message": "Hesap silinemiyor" }, "cannotDeleteAccountDesc": { - "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." + "message": "Hesabınızın sahibi bir kuruluş olduğu için bu işlem tamamlanamadı. Bilgi almak için kuruluş yöneticinizle iletişime geçin." }, "accountDeleted": { "message": "Hesap silindi" @@ -2371,10 +2377,10 @@ "message": "WebAuthn ile doğrula" }, "readSecurityKey": { - "message": "Read security key" + "message": "Güvenlik anahtarını oku" }, "awaitingSecurityKeyInteraction": { - "message": "Awaiting security key interaction..." + "message": "Güvenlik anahtarı etkileşimi bekleniyor..." }, "hideEmail": { "message": "E-posta adresimi alıcılardan gizle." @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Kasa zaman aşımınız, kuruluşunuz tarafından belirlenen kısıtlamaları aşıyor." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Zaman aşımı ayarlarınıza kurumsal ilke gereksinimleri uygulandı" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Kuruluş ilkeleriniz izin verilen maksimum kasa zaman aşımını $HOURS$ saat $MINUTES$ dakika olarak belirlemiş.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Zaman aşımınız kuruluşunuzun belirlediği maksimum süreyi aşıyor: Maksimum $HOURS$ saat $MINUTES$ dakika", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Davet kabul edildi" }, @@ -2666,7 +2701,7 @@ "message": "Parola üretildi" }, "passphraseGenerated": { - "message": "Passphrase generated" + "message": "Parola cümlesi üretildi" }, "usernameGenerated": { "message": "Kullanıcı adı üretildi" @@ -2689,7 +2724,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Güçlü bir parola cümlesi üretmek için en az $RECOMMENDED$ kelime kullanın.", "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": { @@ -2699,7 +2734,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Güçlü bir parola cümlesi üretmek için en az $RECOMMENDED$ kelime kullanın.", "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": { @@ -2731,7 +2766,7 @@ "message": "Bu parolayı kullan" }, "useThisPassphrase": { - "message": "Use this passphrase" + "message": "Bu parola cümlesini kullan" }, "useThisUsername": { "message": "Bu kullanıcı adını kullan" @@ -2992,9 +3027,6 @@ "message": "Karakter sayacını aç/kapat", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Hesabınıza erişmeye mi çalışıyorsunuz?" - }, "accessAttemptBy": { "message": "$EMAIL$ erişim denemesi", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "$DEVICE$ cihazında $EMAIL$ girişi onaylandı", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Başka bir cihazdan giriş isteğini reddettiniz. Yanlışlıkla yaptıysanız aynı cihazdan yeniden giriş yapmayı deneyin." + }, + "webApp": { + "message": "Web uygulaması" + }, + "mobile": { + "message": "Mobil", + "description": "Mobile app" + }, + "extension": { + "message": "Uzantı", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Masaüstü", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Sunucu" + }, + "loginRequest": { + "message": "Giriş isteği" + }, "deviceType": { "message": "Cihaz türü" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Erişimi reddet" }, - "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." - }, "justNow": { "message": "Az önce" }, @@ -3664,19 +3724,19 @@ "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'" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Önce PIN veya parola gerektiğinden biyometrik kilit açılamıyor." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Biyometrik kilit açma şu anda kullanılamıyor." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biyometrik kilit açma, yanlış yapılandırılmış sistem dosyaları nedeniyle kullanılamıyor." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biyometrik kilit açma, yanlış yapılandırılmış sistem dosyaları nedeniyle kullanılamıyor." }, "biometricsStatusHelptextNotEnabledLocally": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Biyometrik kilit açma, Bitwarden masaüstü uygulamasında $EMAIL$ etkin olmadığı için kullanılamıyor.", "placeholders": { "email": { "content": "$1", @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Daha fazla göster" + }, + "showLess": { + "message": "Daha az göster" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index e53e68dea16..a577d9fe868 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -42,7 +42,7 @@ "message": "Шукати у сховищі" }, "resetSearch": { - "message": "Reset search" + "message": "Скинути пошук" }, "addItem": { "message": "Додати запис" @@ -576,7 +576,7 @@ "message": "Копіювати код підтвердження (TOTP)" }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Копіювати $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Двоетапна перевірка" }, + "vaultTimeoutHeader": { + "message": "Час очікування сховища" + }, "vaultTimeout": { "message": "Час очікування сховища" }, "vaultTimeout1": { "message": "Час очікування" }, + "vaultTimeoutAction1": { + "message": "Дія після часу очікування" + }, "vaultTimeoutDesc": { "message": "Оберіть дію, яка виконається після завершення часу очікування сховища." }, @@ -1443,7 +1449,7 @@ "description": "Copy credit card security code (CVV)" }, "cardNumber": { - "message": "card number" + "message": "номер картки" }, "premiumMembership": { "message": "Преміум статус" @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Час очікування сховища перевищує обмеження, встановлені вашою організацією." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "До налаштувань часу очікування застосовано вимоги політики компанії" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Політикою вашої організації встановлено максимальний дозволений час очікування сховища $HOURS$ годин, $MINUTES$ хвилин.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Час очікування перевищує обмеження, встановлене вашою організацією: $HOURS$ год і $MINUTES$ хв максимум", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Запрошення прийнято" }, @@ -2992,9 +3027,6 @@ "message": "Перемкнути лічильник символів", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Ви намагаєтесь отримати доступ до свого облікового запису?" - }, "accessAttemptBy": { "message": "Спроба доступу з $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Запит входу підтверджено для $EMAIL$ на $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були ви, спробуйте ввійти з пристроєм знову." + }, + "webApp": { + "message": "Вебпрограма" + }, + "mobile": { + "message": "Мобільний", + "description": "Mobile app" + }, + "extension": { + "message": "Розширення", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Комп'ютер", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Сервер" + }, + "loginRequest": { + "message": "Запит входу" + }, "deviceType": { "message": "Тип пристрою" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Заборонити доступ" }, - "logInConfirmedForEmailOnDevice": { - "message": "Підтверджено вхід для $EMAIL$ на $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте ввійти з пристроєм знову." - }, "justNow": { "message": "Щойно" }, @@ -4006,10 +4066,16 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { - "message": "Enable Autotype" + "message": "Увімкнути автовведення" }, "enableAutotypeDescription": { - "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." + "message": "Bitwarden не перевіряє місця введення. Переконайтеся, що у вас відкрите правильне вікно і вибрано потрібне поле, перш ніж застосувати комбінацію клавіш." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 8fed49dea39..e97db4388e5 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -324,7 +324,7 @@ "message": "Tháng 12" }, "ex": { - "message": "Ví dụ:", + "message": "ví dụ.", "description": "Short abbreviation for 'example'." }, "title": { @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "Đăng nhập hai bước" }, + "vaultTimeoutHeader": { + "message": "Thời gian mở kho" + }, "vaultTimeout": { "message": "Đóng kho sau" }, "vaultTimeout1": { "message": "Quá hạn" }, + "vaultTimeoutAction1": { + "message": "Hành động sau khi đóng kho" + }, "vaultTimeoutDesc": { "message": "Chọn thời điểm đóng kho của bạn và thực hiện hành động đã được chọn." }, @@ -1384,7 +1390,7 @@ "description": "Copy to clipboard" }, "checkForUpdates": { - "message": "Kiểm tra cập nhật mới" + "message": "Kiểm tra cập nhật…" }, "version": { "message": "Phiên bản $VERSION_NUM$", @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "Thời gian mở kho vượt quá giới hạn do tổ chức của bạn đặt ra." }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Các yêu cầu chính sách của doanh nghiệp đã được áp dụng cho các tùy chọn thời gian mở kho của bạn" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Tổ chức của bạn đã đặt thời gian mở kho tối đa là $HOURS$ giờ và $MINUTES$ phút.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Thời gian mở kho đã vượt quá giới hạn do tổ chức của bạn đặt ra: Tối đa $HOURS$ giờ và $MINUTES$ phút", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "Đã chấp nhận lời mời" }, @@ -2992,9 +3027,6 @@ "message": "Bật/tắt hiển thị số ký tự", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Bạn đang cố gắng truy cập tài khoản của mình?" - }, "accessAttemptBy": { "message": "Cố gắng truy cập bởi $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Đã phê duyệt yêu cầu đăng nhập cho $EMAIL$ trên $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "Bạn đã từ chối một lần đăng nhập từ thiết bị khác. Nếu đó là bạn, hãy thử đăng nhập lại bằng thiết bị đó." + }, + "webApp": { + "message": "Ứng dụng web" + }, + "mobile": { + "message": "Di động", + "description": "Mobile app" + }, + "extension": { + "message": "Tiện ích mở rộng", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Máy tính", + "description": "Desktop app" + }, + "cli": { + "message": "Giao diện dòng lệnh (CLI)" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Máy chủ" + }, + "loginRequest": { + "message": "Yêu cầu đăng nhập" + }, "deviceType": { "message": "Loại thiết bị" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Từ chối truy cập" }, - "logInConfirmedForEmailOnDevice": { - "message": "Đã xác nhận đăng nhập cho $EMAIL$ trên $DEVICE$", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "Bạn đã từ chối một lần đăng nhập từ thiết bị khác. Nếu thực sự là bạn, hãy thử đăng nhập lại bằng thiết bị đó." - }, "justNow": { "message": "Vừa xong" }, @@ -3709,7 +3769,7 @@ "message": "Tải lên" }, "authorize": { - "message": "Uỷ quyền" + "message": "Ủy quyền" }, "deny": { "message": "Từ chối" @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Xem thêm" + }, + "showLess": { + "message": "Thu gọn" + }, "enableAutotype": { "message": "Bật tính năng Tự động nhập liệu" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index dbfff50c0e3..3dd11c5d106 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "两步登录" }, + "vaultTimeoutHeader": { + "message": "密码库超时" + }, "vaultTimeout": { "message": "密码库超时时间" }, "vaultTimeout1": { "message": "超时" }, + "vaultTimeoutAction1": { + "message": "超时动作" + }, "vaultTimeoutDesc": { "message": "选择您的密码库何时执行密码库超时动作。" }, @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "您的密码库超时时间超出了组织设置的限制。" }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "企业策略要求已应用到您的超时选项中" + }, + "vaultTimeoutPolicyInEffect": { + "message": "您的组织策略已将您最大允许的密码库超时时间设置为 $HOURS$ 小时 $MINUTES$ 分钟。", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "超时时间超出了您组织设置的限制:最多 $HOURS$ 小时 $MINUTES$ 分钟", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "邀请已接受" }, @@ -2992,9 +3027,6 @@ "message": "字符计数开关", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "您正在尝试访问您的账户吗?" - }, "accessAttemptBy": { "message": "$EMAIL$ 的访问尝试", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "已批准 $EMAIL$ 在 $DEVICE$ 上的登录请求", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "您拒绝了一个来自其他设备的登录尝试。若确实是您本人,请再次使用该设备登录。" + }, + "webApp": { + "message": "网页 App" + }, + "mobile": { + "message": "移动端", + "description": "Mobile app" + }, + "extension": { + "message": "扩展", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "桌面端", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "服务器" + }, + "loginRequest": { + "message": "登录请求" + }, "deviceType": { "message": "设备类型" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "拒绝访问" }, - "logInConfirmedForEmailOnDevice": { - "message": "已确认 $EMAIL$ 在 $DEVICE$ 上的登录", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "您拒绝了一个来自其他设备的登录尝试。若确实是您本人,请尝试再次发起设备登录。" - }, "justNow": { "message": "刚刚" }, @@ -3165,7 +3225,7 @@ "message": "无法完成登录" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { - "message": "You need to log in on a trusted device or ask your administrator to assign you a password." + "message": "您需要在某个受信任的设备上登录,或要求管理员为您分配一个密码。" }, "region": { "message": "区域" @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "显示更多" + }, + "showLess": { + "message": "显示更少" + }, "enableAutotype": { "message": "启用自动类型" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 23b0025f0a6..96b37b1c8fd 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -42,7 +42,7 @@ "message": "搜尋密碼庫" }, "resetSearch": { - "message": "Reset search" + "message": "重置搜尋" }, "addItem": { "message": "新增項目" @@ -211,7 +211,7 @@ "message": "RSA 4096-Bit" }, "sshKeyGenerated": { - "message": "一組SSH金鑰已在之前生成了" + "message": "已生成新的SSH金鑰" }, "sshKeyWrongPassword": { "message": "您輸入的密碼錯誤。" @@ -501,7 +501,7 @@ "description": "This describes a field that is 'linked' (related) to another field." }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "勾選框" }, "linkedValue": { "message": "連結的數值", @@ -712,7 +712,7 @@ "message": "檔案最大為 500MB。" }, "legacyEncryptionUnsupported": { - "message": "Legacy encryption is no longer supported. Please contact support to recover your account." + "message": "不再支援舊版加密。請聯繫支援團隊以恢復您的帳號。" }, "editedFolder": { "message": "資料夾已儲存" @@ -748,7 +748,7 @@ "message": "登入 Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "輸入傳送到您電子郵件信箱的驗證碼" }, "enterTheCodeFromYourAuthenticatorApp": { "message": "請輸入您驗證器應用程式中的代碼" @@ -1225,12 +1225,18 @@ "twoStepLogin": { "message": "兩步驟登入" }, + "vaultTimeoutHeader": { + "message": "Vault timeout" + }, "vaultTimeout": { "message": "密碼庫逾時時間" }, "vaultTimeout1": { "message": "逾時" }, + "vaultTimeoutAction1": { + "message": "Timeout action" + }, "vaultTimeoutDesc": { "message": "選擇密碼庫何時執行密碼庫逾時動作。" }, @@ -1838,7 +1844,7 @@ "message": "要求在啟動應用程式時輸入密碼或 PIN 碼" }, "requirePasswordWithoutPinOnStart": { - "message": "Require password on app start" + "message": "要求在啟動應用程式時輸入密碼" }, "lockWithMasterPassOnRestart1": { "message": "重啟後使用主密碼鎖定" @@ -1856,7 +1862,7 @@ "message": "無法刪除帳號" }, "cannotDeleteAccountDesc": { - "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." + "message": "由於您的帳號由一個機構持有,因此無法完成此操作。請聯絡您的機構管理員以獲取更多詳情。" }, "accountDeleted": { "message": "帳戶已刪除" @@ -1978,7 +1984,7 @@ } }, "cardDetails": { - "message": "Card details" + "message": "信用卡資料" }, "cardBrandDetails": { "message": "$BRAND$ details", @@ -1993,10 +1999,10 @@ "message": "Learn more about authenticators" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "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." @@ -2511,6 +2517,35 @@ "vaultTimeoutTooLarge": { "message": "您的密碼庫逾時時間超過組織設定的限制。" }, + "vaultTimeoutPolicyAffectingOptions": { + "message": "Enterprise policy requirements have been applied to your timeout options" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyMaximumError": { + "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, "inviteAccepted": { "message": "邀請已接受" }, @@ -2539,7 +2574,7 @@ "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." }, "organizationName": { - "message": "Organization name" + "message": "機構名稱" }, "keyConnectorDomain": { "message": "Key Connector domain" @@ -2731,7 +2766,7 @@ "message": "使用此密碼" }, "useThisPassphrase": { - "message": "Use this passphrase" + "message": "使用此密碼" }, "useThisUsername": { "message": "使用此使用者名稱" @@ -2764,7 +2799,7 @@ "message": "使用外部轉寄服務產生一個電子郵件別名。" }, "forwarderDomainName": { - "message": "Email domain", + "message": "電郵域名", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -2786,7 +2821,7 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "由 Bitwarden 生成。", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { @@ -2959,7 +2994,7 @@ "message": "Unlock Bitwarden on your device or on the " }, "notificationSentDeviceAnchor": { - "message": "web app" + "message": "網頁應用程式" }, "notificationSentDevicePart2": { "message": "Make sure the Fingerprint phrase matches the one below before approving." @@ -2992,9 +3027,6 @@ "message": "切換字元計數", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, - "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" - }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", "placeholders": { @@ -3004,6 +3036,50 @@ } } }, + "loginRequestApprovedForEmailOnDevice": { + "message": "Login request approved for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "Web app - Chrome" + } + } + }, + "youDeniedLoginAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + }, + "webApp": { + "message": "Web app" + }, + "mobile": { + "message": "Mobile", + "description": "Mobile app" + }, + "extension": { + "message": "Extension", + "description": "Browser extension/addon" + }, + "desktop": { + "message": "Desktop", + "description": "Desktop app" + }, + "cli": { + "message": "CLI" + }, + "sdk": { + "message": "SDK", + "description": "Software Development Kit" + }, + "server": { + "message": "Server" + }, + "loginRequest": { + "message": "Login request" + }, "deviceType": { "message": "裝置類別" }, @@ -3019,22 +3095,6 @@ "denyAccess": { "message": "Deny access" }, - "logInConfirmedForEmailOnDevice": { - "message": "已確認 $EMAIL$ 在 $DEVICE$ 上的登入", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - }, - "device": { - "content": "$2", - "example": "iOS" - } - } - }, - "youDeniedALogInAttemptFromAnotherDevice": { - "message": "您拒絕了來自其他裝置的登入嘗試。如果這確實是您本人,請嘗試再次使用該裝置登入。" - }, "justNow": { "message": "剛剛" }, @@ -3211,13 +3271,13 @@ "message": "Trust organization" }, "trust": { - "message": "Trust" + "message": "信任" }, "doNotTrust": { - "message": "Do not trust" + "message": "不信任" }, "organizationNotTrusted": { - "message": "Organization is not trusted" + "message": "機構不被信任" }, "emergencyAccessTrustWarning": { "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" @@ -3688,10 +3748,10 @@ "message": "基於不明原因,生物辨識解鎖無法使用。" }, "itemDetails": { - "message": "Item details" + "message": "項目詳細資訊" }, "itemName": { - "message": "Item name" + "message": "項目名稱" }, "loginCredentials": { "message": "Login credentials" @@ -3700,7 +3760,7 @@ "message": "Additional options" }, "itemHistory": { - "message": "Item history" + "message": "項目歷史記錄" }, "lastEdited": { "message": "Last edited" @@ -4006,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 44ee6ec862d..6daff35e115 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.8.0", + "version": "2025.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.8.0", + "version": "2025.8.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index ffe70bb7bd7..ea2e8affda2 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.8.0", + "version": "2025.8.1", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/scss/tailwind.css b/apps/desktop/src/scss/tailwind.css index 531133affc0..ac2f800c508 100644 --- a/apps/desktop/src/scss/tailwind.css +++ b/apps/desktop/src/scss/tailwind.css @@ -1,5 +1 @@ @import "../../../../libs/components/src/tw-theme.css"; - -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html index 7298133146c..8c73891dc09 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html @@ -49,7 +49,7 @@  {{ organization.name }} - +  {{ organization.name }} - + - Bitwarden + Bitwarden -

+

Sorry, this page isn't available.

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

Go to your web vault diff --git a/apps/web/src/app/admin-console/common/base.events.component.ts b/apps/web/src/app/admin-console/common/base.events.component.ts index 9d06be92eb8..ba315dee7fb 100644 --- a/apps/web/src/app/admin-console/common/base.events.component.ts +++ b/apps/web/src/app/admin-console/common/base.events.component.ts @@ -1,8 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive } from "@angular/core"; +import { Directive, OnDestroy } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { combineLatest, filter, map, Observable, Subject, switchMap, takeUntil } 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 { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { EventView } from "@bitwarden/common/models/view/event.view"; @@ -12,16 +17,17 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; -import { EventService } from "../../core"; +import { EventOptions, EventService } from "../../core"; import { EventExportService } from "../../tools/event-export"; @Directive() -export abstract class BaseEventsComponent { +export abstract class BaseEventsComponent implements OnDestroy { loading = true; loaded = false; events: EventView[]; dirtyDates = true; continuationToken: string; + canUseSM = false; abstract readonly exportFileName: string; @@ -30,6 +36,15 @@ export abstract class BaseEventsComponent { end: new FormControl(null), }); + protected canUseSM$: Observable; + protected activeOrganization$: Observable; + protected organizations$: Observable; + private destroySubject$ = new Subject(); + + protected get destroy$(): Observable { + return this.destroySubject$.asObservable(); + } + constructor( protected eventService: EventService, protected i18nService: I18nService, @@ -38,12 +53,39 @@ export abstract class BaseEventsComponent { protected logService: LogService, protected fileDownloadService: FileDownloadService, private toastService: ToastService, + protected activeRoute: ActivatedRoute, + protected accountService: AccountService, + protected organizationService: OrganizationService, ) { const defaultDates = this.eventService.getDefaultDateFilters(); this.start = defaultDates[0]; this.end = defaultDates[1]; } + protected initBase(): void { + this.organizations$ = this.accountService.activeAccount$.pipe( + filter((account): account is Account => !!account?.id), + switchMap((account) => this.organizationService.organizations$(account.id)), + ); + + this.activeOrganization$ = combineLatest([this.activeRoute.paramMap, this.organizations$]).pipe( + map(([params, orgs]) => orgs.find((org) => org.id === params.get("organizationId"))), + ); + + this.canUseSM$ = this.activeOrganization$.pipe( + map((org) => org?.canAccessSecretsManager ?? false), + ); + + this.canUseSM$.pipe(takeUntil(this.destroy$)).subscribe((value) => { + this.canUseSM = value; + }); + } + + ngOnDestroy(): void { + this.destroySubject$.next(); + this.destroySubject$.complete(); + } + get start(): string { return this.eventsForm.value.start; } @@ -139,7 +181,10 @@ export abstract class BaseEventsComponent { const events = await Promise.all( response.data.map(async (r) => { const userId = r.actingUserId == null ? r.userId : r.actingUserId; - const eventInfo = await this.eventService.getEventInfo(r); + const options = new EventOptions(); + options.disableLink = !this.canUseSM; + + const eventInfo = await this.eventService.getEventInfo(r, options); const user = this.getUserName(r, userId); const userName = user != null ? user.name : this.i18nService.t("unknown"); diff --git a/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.spec.ts b/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.spec.ts index abd99d37355..ad3d0d8169a 100644 --- a/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.spec.ts +++ b/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.spec.ts @@ -1,5 +1,7 @@ import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { newGuid } from "@bitwarden/guid"; import { getNestedCollectionTree, getFlatCollectionTree } from "./collection-utils"; @@ -9,11 +11,17 @@ describe("CollectionUtils Service", () => { // Arrange const collections: CollectionView[] = []; - const parentCollection = new CollectionView(); - parentCollection.name = "Parent"; + const parentCollection = new CollectionView({ + name: "Parent", + organizationId: "orgId" as OrganizationId, + id: newGuid() as CollectionId, + }); - const childCollection = new CollectionView(); - childCollection.name = "Parent/Child"; + const childCollection = new CollectionView({ + name: "Parent/Child", + organizationId: "orgId" as OrganizationId, + id: newGuid() as CollectionId, + }); collections.push(childCollection); collections.push(parentCollection); @@ -41,12 +49,14 @@ describe("CollectionUtils Service", () => { describe("getFlatCollectionTree", () => { it("should flatten a tree node with no children", () => { // Arrange - const collection = new CollectionView(); - collection.name = "Test Collection"; - collection.id = "test-id"; + const collection = new CollectionView({ + name: "Test Collection", + id: "test-id" as CollectionId, + organizationId: "orgId" as OrganizationId, + }); const treeNodes: TreeNode[] = [ - new TreeNode(collection, null), + new TreeNode(collection, {} as TreeNode), ]; // Act @@ -59,23 +69,34 @@ describe("CollectionUtils Service", () => { it("should flatten a tree node with children", () => { // Arrange - const parentCollection = new CollectionView(); - parentCollection.name = "Parent"; - parentCollection.id = "parent-id"; + const parentCollection = new CollectionView({ + name: "Parent", + id: "parent-id" as CollectionId, + organizationId: "orgId" as OrganizationId, + }); - const child1Collection = new CollectionView(); - child1Collection.name = "Child 1"; - child1Collection.id = "child1-id"; + const child1Collection = new CollectionView({ + name: "Child 1", + id: "child1-id" as CollectionId, + organizationId: "orgId" as OrganizationId, + }); - const child2Collection = new CollectionView(); - child2Collection.name = "Child 2"; - child2Collection.id = "child2-id"; + const child2Collection = new CollectionView({ + name: "Child 2", + id: "child2-id" as CollectionId, + organizationId: "orgId" as OrganizationId, + }); - const grandchildCollection = new CollectionView(); - grandchildCollection.name = "Grandchild"; - grandchildCollection.id = "grandchild-id"; + const grandchildCollection = new CollectionView({ + name: "Grandchild", + id: "grandchild-id" as CollectionId, + organizationId: "orgId" as OrganizationId, + }); - const parentNode = new TreeNode(parentCollection, null); + const parentNode = new TreeNode( + parentCollection, + {} as TreeNode, + ); const child1Node = new TreeNode(child1Collection, parentNode); const child2Node = new TreeNode(child2Collection, parentNode); const grandchildNode = new TreeNode(grandchildCollection, child1Node); diff --git a/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts b/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts index 0697659c976..8a0fab520e3 100644 --- a/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts +++ b/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts @@ -22,7 +22,7 @@ export function getNestedCollectionTree( // Collections need to be cloned because ServiceUtils.nestedTraverse actively // modifies the names of collections. // These changes risk affecting collections store in StateService. - const clonedCollections = collections + const clonedCollections: CollectionView[] | CollectionAdminView[] = collections .sort((a, b) => a.name.localeCompare(b.name)) .map(cloneCollection); @@ -37,6 +37,21 @@ export function getNestedCollectionTree( return nodes; } +export function cloneCollection(collection: CollectionView): CollectionView; +export function cloneCollection(collection: CollectionAdminView): CollectionAdminView; +export function cloneCollection( + collection: CollectionView | CollectionAdminView, +): CollectionView | CollectionAdminView { + let cloned; + + if (collection instanceof CollectionAdminView) { + cloned = Object.assign(new CollectionAdminView({ ...collection }), collection); + } else { + cloned = Object.assign(new CollectionView({ ...collection }), collection); + } + return cloned; +} + export function getFlatCollectionTree( nodes: TreeNode[], ): CollectionAdminView[]; @@ -57,32 +72,3 @@ export function getFlatCollectionTree( return [node.node, ...children]; }); } - -function cloneCollection(collection: CollectionView): CollectionView; -function cloneCollection(collection: CollectionAdminView): CollectionAdminView; -function cloneCollection( - collection: CollectionView | CollectionAdminView, -): CollectionView | CollectionAdminView { - let cloned; - - if (collection instanceof CollectionAdminView) { - cloned = new CollectionAdminView(); - cloned.groups = [...collection.groups]; - cloned.users = [...collection.users]; - cloned.assigned = collection.assigned; - cloned.unmanaged = collection.unmanaged; - } else { - cloned = new CollectionView(); - } - - cloned.id = collection.id; - cloned.externalId = collection.externalId; - cloned.hidePasswords = collection.hidePasswords; - cloned.name = collection.name; - cloned.organizationId = collection.organizationId; - cloned.readOnly = collection.readOnly; - cloned.manage = collection.manage; - cloned.type = collection.type; - - return cloned; -} diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.html b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.html index 50d34227b56..9c3e607d6eb 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.html +++ b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.html @@ -34,6 +34,7 @@ [bitMenuTriggerFor]="editCollectionMenu" size="small" type="button" + [label]="'editCollection' | i18n" > diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts index 121a5c03ffe..1be16c65cb8 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts @@ -5,7 +5,7 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; import { CollectionAdminService, @@ -14,6 +14,8 @@ import { } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -99,6 +101,7 @@ export class VaultHeaderComponent { private dialogService: DialogService, private collectionAdminService: CollectionAdminService, private router: Router, + private accountService: AccountService, ) {} get title() { @@ -199,7 +202,14 @@ export class VaultHeaderComponent { async addCollection() { if (this.organization.productTierType === ProductTierType.Free) { - const collections = await this.collectionAdminService.getAll(this.organization.id); + const collections = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.collectionAdminService.collectionAdminViews$(this.organization.id, userId), + ), + ), + ); if (collections.length === this.organization.maxCollections) { this.showFreeOrgUpgradeDialog(); return; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 9653c405490..87f309c6f66 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -79,8 +79,11 @@ import { DecryptionFailureDialogComponent, PasswordRepromptService, } from "@bitwarden/vault"; -import { OrganizationResellerRenewalWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/components/organization-reseller-renewal-warning.component"; -import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/warnings/services/organization-warnings.service"; +import { + OrganizationFreeTrialWarningComponent, + OrganizationResellerRenewalWarningComponent, +} from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; +import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component"; import { BillingNotificationService } from "../../../billing/services/billing-notification.service"; @@ -90,7 +93,6 @@ import { } from "../../../billing/services/reseller-warning.service"; import { TrialFlowService } from "../../../billing/services/trial-flow.service"; import { FreeTrial } from "../../../billing/types/free-trial"; -import { OrganizationFreeTrialWarningComponent } from "../../../billing/warnings/components/organization-free-trial-warning.component"; import { SharedModule } from "../../../shared"; import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections"; import { @@ -363,7 +365,12 @@ export class VaultComponent implements OnInit, OnDestroy { this.allCollectionsWithoutUnassigned$ = this.refresh$.pipe( switchMap(() => organizationId$), - switchMap((orgId) => this.collectionAdminService.getAll(orgId)), + switchMap((orgId) => + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)), + ), + ), shareReplay({ refCount: false, bufferSize: 1 }), ); @@ -386,11 +393,13 @@ export class VaultComponent implements OnInit, OnDestroy { // FIXME: We should not assert that the Unassigned type is a CollectionId. // Instead we should consider representing the Unassigned collection as a different object, given that // it is not actually a collection. - const noneCollection = new CollectionAdminView(); - noneCollection.name = this.i18nService.t("unassigned"); - noneCollection.id = Unassigned as CollectionId; - noneCollection.organizationId = organizationId; - return allCollections.concat(noneCollection); + return allCollections.concat( + new CollectionAdminView({ + name: this.i18nService.t("unassigned"), + id: Unassigned as CollectionId, + organizationId, + }), + ); }), ); @@ -667,6 +676,15 @@ export class VaultComponent implements OnInit, OnDestroy { ) .subscribe(); + organization$ + .pipe( + switchMap((organization) => + this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), + ), + takeUntil(this.destroy$), + ) + .subscribe(); + const freeTrial$ = combineLatest([ organization$, this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)), diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index cbb4e1cf064..be9a85ffe4b 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -150,6 +150,12 @@ > {{ "accessingUsingProvider" | i18n: organization.providerName }} + + diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 89f62ed8975..4b6e9a431b4 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -28,6 +28,11 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getById } from "@bitwarden/common/platform/misc"; import { BannerModule, IconModule, AdminConsoleLogo } from "@bitwarden/components"; +import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; +import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; +import { NonIndividualSubscriber } from "@bitwarden/web-vault/app/billing/types"; +import { TaxIdWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/components"; +import { TaxIdWarningType } from "@bitwarden/web-vault/app/billing/warnings/types"; import { FreeFamiliesPolicyService } from "../../../billing/services/free-families-policy.service"; import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component"; @@ -44,6 +49,9 @@ import { WebLayoutModule } from "../../../layouts/web-layout.module"; IconModule, OrgSwitcherComponent, BannerModule, + TaxIdWarningComponent, + TaxIdWarningComponent, + OrganizationWarningsModule, ], }) export class OrganizationLayoutComponent implements OnInit { @@ -58,7 +66,6 @@ export class OrganizationLayoutComponent implements OnInit { showPaymentAndHistory$: Observable; hideNewOrgButton$: Observable; organizationIsUnmanaged$: Observable; - enterpriseOrganization$: Observable; protected isBreadcrumbEventLogsEnabled$: Observable; protected showSponsoredFamiliesDropdown$: Observable; @@ -69,6 +76,9 @@ export class OrganizationLayoutComponent implements OnInit { textKey: string; }>; + protected subscriber$: Observable; + protected getTaxIdWarning$: () => Observable; + constructor( private route: ActivatedRoute, private organizationService: OrganizationService, @@ -79,6 +89,7 @@ export class OrganizationLayoutComponent implements OnInit { private accountService: AccountService, private freeFamiliesPolicyService: FreeFamiliesPolicyService, private organizationBillingService: OrganizationBillingServiceAbstraction, + private organizationWarningsService: OrganizationWarningsService, ) {} async ngOnInit() { @@ -150,6 +161,20 @@ export class OrganizationLayoutComponent implements OnInit { : { route: "billing/payment-method", textKey: "paymentMethod" }, ), ); + + this.subscriber$ = this.organization$.pipe( + map((organization) => ({ + type: "organization", + data: organization, + })), + ); + + this.getTaxIdWarning$ = () => + this.organization$.pipe( + switchMap((organization) => + this.organizationWarningsService.getTaxIdWarning$(organization), + ), + ); } canShowVaultTab(organization: Organization): boolean { @@ -179,4 +204,6 @@ export class OrganizationLayoutComponent implements OnInit { getReportTabLabel(organization: Organization): string { return organization.useEvents ? "reporting" : "reports"; } + + refreshTaxIdWarning = () => this.organizationWarningsService.refreshTaxIdWarning(); } diff --git a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts index 10f68695e88..8484b05283d 100644 --- a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts @@ -8,6 +8,8 @@ import { firstValueFrom, switchMap } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { EventView } from "@bitwarden/common/models/view/event.view"; @@ -26,7 +28,7 @@ import { EventService } from "../../../core"; import { SharedModule } from "../../../shared"; export interface EntityEventsDialogParams { - entity: "user" | "cipher"; + entity: "user" | "cipher" | "secret" | "project"; entityId: string; organizationId?: string; @@ -72,6 +74,8 @@ export class EntityEventsComponent implements OnInit, OnDestroy { private toastService: ToastService, private router: Router, private activeRoute: ActivatedRoute, + private accountService: AccountService, + protected organizationService: OrganizationService, ) {} async ngOnInit() { @@ -162,6 +166,22 @@ export class EntityEventsComponent implements OnInit, OnDestroy { dates[1], clearExisting ? null : this.continuationToken, ); + } else if (this.params.entity === "secret") { + response = await this.apiService.getEventsSecret( + this.params.organizationId, + this.params.entityId, + dates[0], + dates[1], + clearExisting ? null : this.continuationToken, + ); + } else if (this.params.entity === "project") { + response = await this.apiService.getEventsProject( + this.params.organizationId, + this.params.entityId, + dates[0], + dates[1], + clearExisting ? null : this.continuationToken, + ); } else { response = await this.apiService.getEventsCipher( this.params.entityId, diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 3daa6c17d07..f442d429767 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -1,8 +1,8 @@ // 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 { concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs"; +import { ActivatedRoute } from "@angular/router"; +import { concatMap, firstValueFrom, lastValueFrom, takeUntil } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -34,6 +34,8 @@ import { openChangePlanDialog, } from "../../../billing/organizations/change-plan-dialog.component"; import { EventService } from "../../../core"; +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; import { EventExportService } from "../../../tools/event-export"; import { BaseEventsComponent } from "../../common/base.events.component"; @@ -46,9 +48,8 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { }; @Component({ - selector: "app-org-events", templateUrl: "events.component.html", - standalone: false, + imports: [SharedModule, HeaderModule], }) export class EventsComponent extends BaseEventsComponent implements OnInit, OnDestroy { exportFileName = "org-events"; @@ -59,8 +60,6 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe placeholderEvents = placeholderEvents as EventView[]; private orgUsersUserIdMap = new Map(); - private destroy$ = new Subject(); - readonly ProductTierType = ProductTierType; protected isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$( @@ -74,18 +73,18 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe i18nService: I18nService, exportService: EventExportService, platformUtilsService: PlatformUtilsService, - private router: Router, logService: LogService, private userNamePipe: UserNamePipe, - private organizationService: OrganizationService, + protected organizationService: OrganizationService, private organizationUserApiService: OrganizationUserApiService, private organizationApiService: OrganizationApiServiceAbstraction, private providerService: ProviderService, fileDownloadService: FileDownloadService, toastService: ToastService, - private accountService: AccountService, + protected accountService: AccountService, private dialogService: DialogService, private configService: ConfigService, + protected activeRoute: ActivatedRoute, ) { super( eventService, @@ -95,10 +94,15 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe logService, fileDownloadService, toastService, + activeRoute, + accountService, + organizationService, ); } async ngOnInit() { + this.initBase(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.route.params .pipe( @@ -232,9 +236,4 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe } await this.load(); } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index 101512dea04..cc90d10fb4a 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -80,7 +80,7 @@ bitIconButton="bwi-trash" bitFormButton [bitAction]="delete" - [appA11yTitle]="'delete' | i18n" + [label]="'delete' | i18n" > diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index ca7d07220b2..9b9be4e50b3 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -28,6 +28,7 @@ import { } 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 { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -156,7 +157,11 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); - private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe( + private orgCollections$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.collectionAdminService.collectionAdminViews$(this.organizationId, userId), + ), shareReplay({ refCount: true, bufferSize: 1 }), ); diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.html b/apps/web/src/app/admin-console/organizations/manage/groups.component.html index 4518513ba7d..62d0b5b874b 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.html @@ -46,7 +46,7 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" > @@ -82,7 +82,7 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" > diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index f48860c69a6..23e92056c95 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -253,8 +253,8 @@ export class GroupsComponent { private toCollectionMap( response: ListResponse, ): Observable> { - const collections = response.data.map( - (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)), + const collections = response.data.map((r) => + Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)), ); return this.accountService.activeAccount$.pipe( diff --git a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts index 03b77cfaa71..16543cdb58c 100644 --- a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts @@ -8,6 +8,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { SharedModule } from "../../../shared"; + export type UserConfirmDialogData = { name: string; userId: string; @@ -16,9 +18,8 @@ export type UserConfirmDialogData = { }; @Component({ - selector: "app-user-confirm", templateUrl: "user-confirm.component.html", - standalone: false, + imports: [SharedModule], }) export class UserConfirmComponent implements OnInit { name: string; diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index c6a60165fe1..b951f73d953 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -32,6 +32,7 @@ import { import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -276,9 +277,16 @@ export class MemberDialogComponent implements OnDestroy { ), ); + const collections = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.collectionAdminService.collectionAdminViews$(this.params.organizationId, userId), + ), + ); + combineLatest({ organization: this.organization$, - collections: this.collectionAdminService.getAll(this.params.organizationId), + collections, userDetails: userDetails$, groups: groups$, }) diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 49946806efc..7e0aa465bf3 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -106,7 +106,7 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" *ngIf="showUserManagementControls$ | async" > @@ -350,7 +350,7 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" > diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 2aac4d0a5c8..dedf13720bf 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -10,10 +10,10 @@ import { from, lastValueFrom, map, + merge, Observable, shareReplay, switchMap, - tap, } from "rxjs"; import { @@ -57,12 +57,12 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { ChangePlanDialogResultType, openChangePlanDialog, } from "../../../billing/organizations/change-plan-dialog.component"; -import { OrganizationWarningsService } from "../../../billing/warnings/services"; import { BaseMembersComponent } from "../../common/base-members.component"; import { PeopleTableDataSource } from "../../common/people-table-data-source"; import { GroupApiService } from "../core"; @@ -200,7 +200,14 @@ export class MembersComponent extends BaseMembersComponent this.organization.canManageUsersPassword && !this.organization.hasPublicAndPrivateKeys ) { - const orgShareKey = await this.keyService.getOrgKey(this.organization.id); + const orgShareKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[this.organization.id] ?? null), + ), + ); + const orgKeys = await this.keyService.makeKeyPair(orgShareKey); const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); const response = await this.organizationApiService.updateKeys( @@ -246,11 +253,16 @@ export class MembersComponent extends BaseMembersComponent this.showUserManagementControls$ = organization$.pipe( map((organization) => organization.canManageUsers), ); + organization$ .pipe( + switchMap((organization) => + merge( + this.organizationWarningsService.showInactiveSubscriptionDialog$(organization), + this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), + ), + ), takeUntilDestroyed(), - tap((org) => (this.organization = org)), - switchMap((org) => this.organizationWarningsService.showInactiveSubscriptionDialog$(org)), ) .subscribe(); } @@ -305,7 +317,9 @@ export class MembersComponent extends BaseMembersComponent async getCollectionNameMap() { const response = from(this.apiService.getCollections(this.organization.id)).pipe( map((res) => - res.data.map((r) => new Collection(new CollectionData(r as CollectionDetailsResponse))), + res.data.map((r) => + Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)), + ), ), ); @@ -353,7 +367,13 @@ export class MembersComponent extends BaseMembersComponent this.organizationUserService.confirmUser(this.organization, user, publicKey), ); } else { - const orgKey = await this.keyService.getOrgKey(this.organization.id); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[this.organization.id] ?? null), + ), + ); const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey); const request = new OrganizationUserConfirmRequest(); request.key = key.encryptedString; diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index d9c5ae356a2..efc091cb335 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -4,8 +4,8 @@ import { NgModule } from "@angular/core"; import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { ScrollLayoutDirective } from "@bitwarden/components"; +import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; -import { OrganizationFreeTrialWarningComponent } from "../../../billing/warnings/components"; import { LooseComponentsModule } from "../../../shared"; import { SharedOrganizationModule } from "../shared"; diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts index db94eb2535f..afc16e72373 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts @@ -17,8 +17,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { UserKey, OrgKey, MasterKey } from "@bitwarden/common/types/key"; import { KdfType, KeyService } from "@bitwarden/key-management"; @@ -36,6 +37,8 @@ describe("OrganizationUserResetPasswordService", () => { let organizationUserApiService: MockProxy; let organizationApiService: MockProxy; let i18nService: MockProxy; + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; beforeAll(() => { keyService = mock(); @@ -44,6 +47,7 @@ describe("OrganizationUserResetPasswordService", () => { organizationUserApiService = mock(); organizationApiService = mock(); i18nService = mock(); + accountService = mockAccountServiceWith(mockUserId); sut = new OrganizationUserResetPasswordService( keyService, @@ -52,6 +56,7 @@ describe("OrganizationUserResetPasswordService", () => { organizationUserApiService, organizationApiService, i18nService, + accountService, ); }); @@ -142,7 +147,10 @@ describe("OrganizationUserResetPasswordService", () => { const mockRandomBytes = new Uint8Array(64) as CsprngArray; const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey; - keyService.getOrgKey.mockResolvedValue(mockOrgKey); + keyService.orgKeys$.mockReturnValue( + of({ [mockOrgId]: mockOrgKey } as Record), + ); + encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes); encryptService.rsaDecrypt.mockResolvedValue(mockRandomBytes); @@ -170,7 +178,7 @@ describe("OrganizationUserResetPasswordService", () => { }); it("should throw an error if the org key is null", async () => { - keyService.getOrgKey.mockResolvedValue(null); + keyService.orgKeys$.mockReturnValue(of(null)); await expect( sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId), ).rejects.toThrow(); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index a1727a8cc59..df5e7e8a25c 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { OrganizationUserApiService, @@ -10,6 +10,8 @@ import { } from "@bitwarden/admin-console/common"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, @@ -47,6 +49,7 @@ export class OrganizationUserResetPasswordService private organizationUserApiService: OrganizationUserApiService, private organizationApiService: OrganizationApiServiceAbstraction, private i18nService: I18nService, + private accountService: AccountService, ) {} /** @@ -111,7 +114,14 @@ export class OrganizationUserResetPasswordService } // Decrypt Organization's encrypted Private Key with org key - const orgSymKey = await this.keyService.getOrgKey(orgId); + const orgSymKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[orgId as OrganizationId] ?? null), + ), + ); + if (orgSymKey == null) { throw new Error("No org key found"); } diff --git a/apps/web/src/app/admin-console/organizations/organization.module.ts b/apps/web/src/app/admin-console/organizations/organization.module.ts index 687361760c9..d956174149b 100644 --- a/apps/web/src/app/admin-console/organizations/organization.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization.module.ts @@ -2,6 +2,7 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { NgModule } from "@angular/core"; import { ScrollLayoutDirective } from "@bitwarden/components"; +import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; import { LooseComponentsModule } from "../../shared"; @@ -21,6 +22,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; LooseComponentsModule, ScrollingModule, ScrollLayoutDirective, + OrganizationWarningsModule, ], declarations: [GroupsComponent, GroupAddEditComponent], }) diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index ecb2dbc54a2..21424e86521 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -8,6 +8,7 @@ import { firstValueFrom, from, lastValueFrom, + map, of, Subject, switchMap, @@ -28,6 +29,7 @@ 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 { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -179,7 +181,13 @@ export class AccountComponent implements OnInit, OnDestroy { // Backfill pub/priv key if necessary if (!this.org.hasPublicAndPrivateKeys) { - const orgShareKey = await this.keyService.getOrgKey(this.organizationId); + const orgShareKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[this.organizationId as OrganizationId] ?? null), + ), + ); const orgKeys = await this.keyService.makeKeyPair(orgShareKey); request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts index a803f6ef7b5..5cb61197b99 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts @@ -51,7 +51,7 @@ const render: Story["render"] = (args) => ({ buttonType="danger" size="default" title="Delete" - aria-label="Delete"> + label="Delete"> `, diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html index e9b7ba39aa5..116af15f579 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html @@ -50,7 +50,7 @@ >
- +
{{ item.labelName }} @@ -58,7 +58,10 @@ {{ "invited" | i18n }}
-
+
{{ $any(item).email }}
@@ -77,10 +80,10 @@ - {{ item.labelName }} {{ "permission" | i18n }} @@ -119,7 +122,7 @@ type="button" bitIconButton="bwi-close" buttonType="muted" - appA11yTitle="{{ 'remove' | i18n }} {{ item.labelName }}" + label="{{ 'remove' | i18n }} {{ item.labelName }}" [disabled]="disabled" (click)="selectionList.deselectItem(item.id); handleBlur()" > diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html index 4a91fcc2a41..dec257b3741 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html @@ -143,7 +143,7 @@ buttonType="danger" class="tw-ml-auto" bitFormButton - [appA11yTitle]="'delete' | i18n" + [label]="'delete' | i18n" [bitAction]="delete" [disabled]="loading" > diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index a0964a90fca..59d042cae52 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -240,9 +240,15 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { return this.groupService.getAll(orgId); }), ); + + const collections = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)), + ); + combineLatest({ organization: organization$, - collections: this.collectionAdminService.getAll(orgId), + collections, groups: groups$, users: this.organizationUserApiService.getAllMiniUserDetails(orgId), }) @@ -393,9 +399,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { return; } - const collectionView = new CollectionAdminView(); - collectionView.id = this.params.collectionId; - collectionView.organizationId = this.formGroup.controls.selectedOrg.value; + const parent = this.formGroup.controls.parent?.value; + const collectionView = new CollectionAdminView({ + id: this.params.collectionId as CollectionId, + organizationId: this.formGroup.controls.selectedOrg.value, + name: parent + ? `${parent}/${this.formGroup.controls.name.value}` + : this.formGroup.controls.name.value, + }); collectionView.externalId = this.formGroup.controls.externalId.value; collectionView.groups = this.formGroup.controls.access.value .filter((v) => v.type === AccessItemType.Group) @@ -404,13 +415,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { .filter((v) => v.type === AccessItemType.Member) .map(convertToSelectionView); - const parent = this.formGroup.controls.parent.value; - if (parent) { - collectionView.name = `${parent}/${this.formGroup.controls.name.value}`; - } else { - collectionView.name = this.formGroup.controls.name.value; - } - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const savedCollection = await this.collectionAdminService.save(collectionView, userId); diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts index b0c89cd30ab..2ca566a0af2 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts @@ -1,12 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CommonModule } from "@angular/common"; import { Component, inject } from "@angular/core"; import { Params } from "@angular/router"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { OrganizationSponsorshipResponse } from "@bitwarden/common/admin-console/models/response/organization-sponsorship.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { Icons, ToastService } from "@bitwarden/components"; +import { IconModule, Icons, ToastService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { BaseAcceptComponent } from "../../../common/base.accept.component"; @@ -16,9 +18,8 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component"; * personal email address." - https://bitwarden.com/learning/free-families-plan-for-enterprise/ */ @Component({ - selector: "app-accept-family-sponsorship", templateUrl: "accept-family-sponsorship.component.html", - standalone: false, + imports: [CommonModule, I18nPipe, IconModule], }) export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent { protected logo = Icons.BitwardenLogo; diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 6240fd4eec4..1cb95250611 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -12,6 +12,7 @@ import { EventUploadService } from "@bitwarden/common/abstractions/event/event-u import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; @@ -89,6 +90,7 @@ export class AppComponent implements OnDestroy, OnInit { private deviceTrustToastService: DeviceTrustToastService, private readonly destroy: DestroyRef, private readonly documentLangSetter: DocumentLangSetter, + private readonly tokenService: TokenService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -297,6 +299,7 @@ export class AppComponent implements OnDestroy, OnInit { await this.searchService.clearIndex(userId); this.authService.logOut(async () => { await this.stateService.clean({ userId: userId }); + await this.tokenService.clearAccessToken(userId); await this.accountService.clean(userId); await this.accountService.switchAccount(null); diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html index 6e87d66d18b..1c04c03a8d2 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html @@ -62,7 +62,7 @@ buttonType="danger" [bitAction]="delete" *ngIf="editMode" - appA11yTitle="{{ 'delete' | i18n }}" + label="{{ 'delete' | i18n }}" > diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html index 8a802e4f6af..70165a94fc3 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html @@ -89,7 +89,7 @@ @@ -212,7 +212,7 @@ diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html index 1df1e52f7e4..dbad422a32e 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html @@ -39,7 +39,7 @@ type="button" buttonType="danger" (click)="remove(i)" - appA11yTitle="{{ 'remove' | i18n }}" + label="{{ 'remove' | i18n }}" >
diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index 9e44cc7a713..2c4fa7f447c 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -2,14 +2,15 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { VerifyEmailRequest } from "@bitwarden/common/models/request/verify-email.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ToastService } from "@bitwarden/components"; @Component({ @@ -25,7 +26,7 @@ export class VerifyEmailTokenComponent implements OnInit { private route: ActivatedRoute, private apiService: ApiService, private logService: LogService, - private stateService: StateService, + private tokenService: TokenService, private toastService: ToastService, ) {} @@ -37,7 +38,7 @@ export class VerifyEmailTokenComponent implements OnInit { await this.apiService.postAccountVerifyEmailToken( new VerifyEmailRequest(qParams.userId, qParams.token), ); - if (await this.stateService.getIsAuthenticated()) { + if (await firstValueFrom(this.tokenService.hasAccessToken$(qParams.userId))) { await this.apiService.refreshIdentityToken(); } this.toastService.showToast({ diff --git a/apps/web/src/app/billing/clients/index.ts b/apps/web/src/app/billing/clients/index.ts new file mode 100644 index 00000000000..ff962abcbf3 --- /dev/null +++ b/apps/web/src/app/billing/clients/index.ts @@ -0,0 +1,2 @@ +export * from "./organization-billing.client"; +export * from "./subscriber-billing.client"; diff --git a/apps/web/src/app/billing/clients/organization-billing.client.ts b/apps/web/src/app/billing/clients/organization-billing.client.ts new file mode 100644 index 00000000000..a8b3b31a4a4 --- /dev/null +++ b/apps/web/src/app/billing/clients/organization-billing.client.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrganizationWarningsResponse } from "@bitwarden/web-vault/app/billing/organizations/warnings/types"; + +@Injectable() +export class OrganizationBillingClient { + constructor(private apiService: ApiService) {} + + getWarnings = async (organizationId: OrganizationId): Promise => { + const response = await this.apiService.send( + "GET", + `/organizations/${organizationId}/billing/vnext/warnings`, + null, + true, + true, + ); + + return new OrganizationWarningsResponse(response); + }; +} diff --git a/apps/web/src/app/billing/services/billing.client.ts b/apps/web/src/app/billing/clients/subscriber-billing.client.ts similarity index 72% rename from apps/web/src/app/billing/services/billing.client.ts rename to apps/web/src/app/billing/clients/subscriber-billing.client.ts index 69f82eab19a..18ca215ef0c 100644 --- a/apps/web/src/app/billing/services/billing.client.ts +++ b/apps/web/src/app/billing/clients/subscriber-billing.client.ts @@ -10,7 +10,7 @@ import { MaskedPaymentMethodResponse, TokenizedPaymentMethod, } from "../payment/types"; -import { BillableEntity } from "../types"; +import { BitwardenSubscriber } from "../types"; type Result = | { @@ -23,28 +23,28 @@ type Result = }; @Injectable() -export class BillingClient { +export class SubscriberBillingClient { constructor(private apiService: ApiService) {} - private getEndpoint = (entity: BillableEntity): string => { - switch (entity.type) { + private getEndpoint = (subscriber: BitwardenSubscriber): string => { + switch (subscriber.type) { case "account": { return "/account/billing/vnext"; } case "organization": { - return `/organizations/${entity.data.id}/billing/vnext`; + return `/organizations/${subscriber.data.id}/billing/vnext`; } case "provider": { - return `/providers/${entity.data.id}/billing/vnext`; + return `/providers/${subscriber.data.id}/billing/vnext`; } } }; addCreditWithBitPay = async ( - owner: BillableEntity, + subscriber: BitwardenSubscriber, credit: { amount: number; redirectUrl: string }, ): Promise> => { - const path = `${this.getEndpoint(owner)}/credit/bitpay`; + const path = `${this.getEndpoint(subscriber)}/credit/bitpay`; try { const data = await this.apiService.send("POST", path, credit, true, true); return { @@ -62,29 +62,31 @@ export class BillingClient { } }; - getBillingAddress = async (owner: BillableEntity): Promise => { - const path = `${this.getEndpoint(owner)}/address`; + getBillingAddress = async (subscriber: BitwardenSubscriber): Promise => { + const path = `${this.getEndpoint(subscriber)}/address`; const data = await this.apiService.send("GET", path, null, true, true); return data ? new BillingAddressResponse(data) : null; }; - getCredit = async (owner: BillableEntity): Promise => { - const path = `${this.getEndpoint(owner)}/credit`; + getCredit = async (subscriber: BitwardenSubscriber): Promise => { + const path = `${this.getEndpoint(subscriber)}/credit`; const data = await this.apiService.send("GET", path, null, true, true); return data ? (data as number) : null; }; - getPaymentMethod = async (owner: BillableEntity): Promise => { - const path = `${this.getEndpoint(owner)}/payment-method`; + getPaymentMethod = async ( + subscriber: BitwardenSubscriber, + ): Promise => { + const path = `${this.getEndpoint(subscriber)}/payment-method`; const data = await this.apiService.send("GET", path, null, true, true); return data ? new MaskedPaymentMethodResponse(data).value : null; }; updateBillingAddress = async ( - owner: BillableEntity, + subscriber: BitwardenSubscriber, billingAddress: BillingAddress, ): Promise> => { - const path = `${this.getEndpoint(owner)}/address`; + const path = `${this.getEndpoint(subscriber)}/address`; try { const data = await this.apiService.send("PUT", path, billingAddress, true, true); return { @@ -103,11 +105,11 @@ export class BillingClient { }; updatePaymentMethod = async ( - owner: BillableEntity, + subscriber: BitwardenSubscriber, paymentMethod: TokenizedPaymentMethod, billingAddress: Pick | null, ): Promise> => { - const path = `${this.getEndpoint(owner)}/payment-method`; + const path = `${this.getEndpoint(subscriber)}/payment-method`; try { const request = { ...paymentMethod, @@ -130,10 +132,10 @@ export class BillingClient { }; verifyBankAccount = async ( - owner: BillableEntity, + subscriber: BitwardenSubscriber, descriptorCode: string, ): Promise> => { - const path = `${this.getEndpoint(owner)}/payment-method/verify-bank-account`; + const path = `${this.getEndpoint(subscriber)}/payment-method/verify-bank-account`; try { const data = await this.apiService.send("POST", path, { descriptorCode }, true, true); return { diff --git a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.html b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.html index c10590d8b1b..5bb47cd8a2e 100644 --- a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.html +++ b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.html @@ -12,13 +12,13 @@ } @else { diff --git a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts index 4a4d0f60c0b..9f46d9d3909 100644 --- a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts +++ b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts @@ -20,13 +20,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; +import { SubscriberBillingClient } from "../../clients"; import { DisplayAccountCreditComponent, DisplayPaymentMethodComponent, } from "../../payment/components"; import { MaskedPaymentMethod } from "../../payment/types"; -import { BillingClient } from "../../services"; -import { accountToBillableEntity, BillableEntity } from "../../types"; +import { mapAccountToSubscriber, BitwardenSubscriber } from "../../types"; class RedirectError { constructor( @@ -36,7 +36,7 @@ class RedirectError { } type View = { - account: BillableEntity; + account: BitwardenSubscriber; paymentMethod: MaskedPaymentMethod | null; credit: number | null; }; @@ -50,7 +50,7 @@ type View = { HeaderModule, SharedModule, ], - providers: [BillingClient], + providers: [SubscriberBillingClient], }) export class AccountPaymentDetailsComponent { private viewState$ = new BehaviorSubject(null); @@ -68,7 +68,7 @@ export class AccountPaymentDetailsComponent { }), ), ), - accountToBillableEntity, + mapAccountToSubscriber, switchMap(async (account) => { const [paymentMethod, credit] = await Promise.all([ this.billingClient.getPaymentMethod(account), @@ -100,7 +100,7 @@ export class AccountPaymentDetailsComponent { constructor( private accountService: AccountService, private activatedRoute: ActivatedRoute, - private billingClient: BillingClient, + private billingClient: SubscriberBillingClient, private configService: ConfigService, private router: Router, ) {} diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.html b/apps/web/src/app/billing/members/free-bitwarden-families.component.html index ddf7c506745..697a5963a71 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.html +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.html @@ -53,7 +53,7 @@ bitIconButton="bwi-ellipsis-v" buttonType="main" [bitMenuTriggerFor]="appListDropdown" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" > @if (!isSelfHosted && !sponsoredFamily.validUntil) { diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html index a09000ef55f..465a50ec8c3 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html @@ -33,7 +33,7 @@ showToast [valueLabel]="'billingSyncKey' | i18n" [appCopyClick]="clientSecret" - [appA11yTitle]="'copyValue' | i18n" + [label]="'copyValue' | i18n" >
diff --git a/apps/web/src/app/billing/organizations/billing-sync-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-key.component.html index 9736351deca..94a81140344 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-key.component.html @@ -33,7 +33,7 @@ bitIconButton="bwi-trash" bitFormButton [bitAction]="deleteConnection" - appA11yTitle="{{ 'delete' | i18n }}" + label="{{ 'delete' | i18n }}" > 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 ace3d749a3f..f899b8eccb4 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 @@ -359,7 +359,7 @@ type="button" [bitIconButton]="totalOpened ? 'bwi-angle-down' : 'bwi-angle-up'" size="small" - aria-hidden="true" + [label]="totalOpened ? ('hidePricingSummary' | i18n) : ('showPricingSummary' | i18n)" >

diff --git a/apps/web/src/app/billing/organizations/change-plan.component.html b/apps/web/src/app/billing/organizations/change-plan.component.html index 75a12122d19..1cd15a7c836 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.html +++ b/apps/web/src/app/billing/organizations/change-plan.component.html @@ -8,7 +8,7 @@ type="button" size="small" class="tw-float-right" - appA11yTitle="{{ 'cancel' | i18n }}" + label="{{ 'cancel' | i18n }}" (click)="cancel()" >

{{ "changeBillingPlan" | i18n }}

diff --git a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.html b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.html index 17f4349fdd5..cd31f1f33be 100644 --- a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.html +++ b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.html @@ -21,19 +21,20 @@ } @else { diff --git a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts index e357444b943..d1dfea40fe2 100644 --- a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts +++ b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts @@ -1,8 +1,9 @@ -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { BehaviorSubject, catchError, + combineLatest, EMPTY, filter, firstValueFrom, @@ -11,8 +12,12 @@ import { map, merge, Observable, + of, shareReplay, + Subject, switchMap, + take, + takeUntil, tap, } from "rxjs"; @@ -26,19 +31,26 @@ 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"; - -import { HeaderModule } from "../../../layouts/header/header.module"; -import { SharedModule } from "../../../shared"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; +import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; +import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { ChangePaymentMethodDialogComponent, DisplayAccountCreditComponent, DisplayBillingAddressComponent, DisplayPaymentMethodComponent, -} from "../../payment/components"; -import { BillingAddress, MaskedPaymentMethod } from "../../payment/types"; -import { BillingClient } from "../../services"; -import { BillableEntity, organizationToBillableEntity } from "../../types"; -import { OrganizationFreeTrialWarningComponent } from "../../warnings/components"; +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { + BillingAddress, + MaskedPaymentMethod, +} from "@bitwarden/web-vault/app/billing/payment/types"; +import { + BitwardenSubscriber, + mapOrganizationToSubscriber, +} from "@bitwarden/web-vault/app/billing/types"; +import { TaxIdWarningType } from "@bitwarden/web-vault/app/billing/warnings/types"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; class RedirectError { constructor( @@ -48,93 +60,100 @@ class RedirectError { } type View = { - organization: BillableEntity; + organization: BitwardenSubscriber; paymentMethod: MaskedPaymentMethod | null; billingAddress: BillingAddress | null; credit: number | null; + taxIdWarning: TaxIdWarningType | null; }; @Component({ templateUrl: "./organization-payment-details.component.html", standalone: true, imports: [ - DisplayBillingAddressComponent, DisplayAccountCreditComponent, + DisplayBillingAddressComponent, DisplayPaymentMethodComponent, HeaderModule, OrganizationFreeTrialWarningComponent, SharedModule, ], - providers: [BillingClient], }) -export class OrganizationPaymentDetailsComponent implements OnInit { - @ViewChild(OrganizationFreeTrialWarningComponent) - organizationFreeTrialWarningComponent!: OrganizationFreeTrialWarningComponent; - +export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { private viewState$ = new BehaviorSubject(null); - private load$: Observable = this.accountService.activeAccount$ - .pipe( - getUserId, - switchMap((userId) => - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.activatedRoute.snapshot.params.organizationId)), - ), - ) - .pipe( - switchMap((organization) => - this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => { - if (!managePaymentDetailsOutsideCheckout) { - throw new RedirectError(["../payment-method"], this.activatedRoute); - } - return organization; - }), - ), - ), - organizationToBillableEntity, - switchMap(async (organization) => { - const [paymentMethod, billingAddress, credit] = await Promise.all([ - this.billingClient.getPaymentMethod(organization), - this.billingClient.getBillingAddress(organization), - this.billingClient.getCredit(organization), - ]); + protected organization$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.activatedRoute.snapshot.params.organizationId)), + ), + filter((organization): organization is Organization => !!organization), + ); - return { - organization, - paymentMethod, - billingAddress, - credit, - }; - }), - catchError((error: unknown) => { - if (error instanceof RedirectError) { - return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe( - switchMap(() => EMPTY), - ); - } - throw error; - }), - ); + private load$: Observable = this.organization$.pipe( + switchMap((organization) => + this.configService + .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) + .pipe( + map((managePaymentDetailsOutsideCheckout) => { + if (!managePaymentDetailsOutsideCheckout) { + throw new RedirectError(["../payment-method"], this.activatedRoute); + } + return organization; + }), + ), + ), + mapOrganizationToSubscriber, + switchMap(async (organization) => { + const getTaxIdWarning = firstValueFrom( + this.organizationWarningsService.getTaxIdWarning$(organization.data as Organization), + ); + + const [paymentMethod, billingAddress, credit, taxIdWarning] = await Promise.all([ + this.subscriberBillingClient.getPaymentMethod(organization), + this.subscriberBillingClient.getBillingAddress(organization), + this.subscriberBillingClient.getCredit(organization), + getTaxIdWarning, + ]); + + return { + organization, + paymentMethod, + billingAddress, + credit, + taxIdWarning, + }; + }), + catchError((error: unknown) => { + if (error instanceof RedirectError) { + return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe( + switchMap(() => EMPTY), + ); + } + throw error; + }), + ); view$: Observable = merge( this.load$.pipe(tap((view) => this.viewState$.next(view))), this.viewState$.pipe(filter((view): view is View => view !== null)), ).pipe(shareReplay({ bufferSize: 1, refCount: true })); - organization$ = this.view$.pipe(map((view) => view.organization.data as Organization)); + private destroy$ = new Subject(); + + protected enableTaxIdWarning!: boolean; constructor( private accountService: AccountService, private activatedRoute: ActivatedRoute, - private billingClient: BillingClient, private configService: ConfigService, private dialogService: DialogService, private organizationService: OrganizationService, + private organizationWarningsService: OrganizationWarningsService, private router: Router, + private subscriberBillingClient: SubscriberBillingClient, ) {} async ngOnInit() { @@ -145,24 +164,66 @@ export class OrganizationPaymentDetailsComponent implements OnInit { history.replaceState({ ...history.state, launchPaymentModalAutomatically: false }, ""); await this.changePaymentMethod(); } + + this.enableTaxIdWarning = await this.configService.getFeatureFlag( + FeatureFlag.PM22415_TaxIDWarnings, + ); + + if (this.enableTaxIdWarning) { + this.organizationWarningsService.taxIdWarningRefreshed$ + .pipe( + switchMap((warning) => + combineLatest([ + of(warning), + this.organization$.pipe(take(1)).pipe( + mapOrganizationToSubscriber, + switchMap((organization) => + this.subscriberBillingClient.getBillingAddress(organization), + ), + ), + ]), + ), + takeUntil(this.destroy$), + ) + .subscribe(([taxIdWarning, billingAddress]) => { + if (this.viewState$.value) { + this.viewState$.next({ + ...this.viewState$.value, + taxIdWarning, + billingAddress, + }); + } + }); + } + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } changePaymentMethod = async () => { const view = await firstValueFrom(this.view$); const dialogRef = ChangePaymentMethodDialogComponent.open(this.dialogService, { data: { - owner: view.organization, + subscriber: view.organization, }, }); const result = await lastValueFrom(dialogRef.closed); if (result?.type === "success") { await this.setPaymentMethod(result.paymentMethod); - this.organizationFreeTrialWarningComponent.refresh(); + this.organizationWarningsService.refreshFreeTrialWarning(); } }; setBillingAddress = (billingAddress: BillingAddress) => { if (this.viewState$.value) { + if ( + this.enableTaxIdWarning && + this.viewState$.value.billingAddress?.taxId !== billingAddress.taxId + ) { + this.organizationWarningsService.refreshTaxIdWarning(); + } this.viewState$.next({ ...this.viewState$.value, billingAddress, @@ -174,7 +235,7 @@ export class OrganizationPaymentDetailsComponent implements OnInit { if (this.viewState$.value) { const billingAddress = this.viewState$.value.billingAddress ?? - (await this.billingClient.getBillingAddress(this.viewState$.value.organization)); + (await this.subscriberBillingClient.getBillingAddress(this.viewState$.value.organization)); this.viewState$.next({ ...this.viewState$.value, diff --git a/apps/web/src/app/billing/organizations/warnings/components/index.ts b/apps/web/src/app/billing/organizations/warnings/components/index.ts new file mode 100644 index 00000000000..1e1e0682e62 --- /dev/null +++ b/apps/web/src/app/billing/organizations/warnings/components/index.ts @@ -0,0 +1,2 @@ +export * from "./organization-free-trial-warning.component"; +export * from "./organization-reseller-renewal-warning.component"; diff --git a/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts b/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts similarity index 54% rename from apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts rename to apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts index a7ce53c9998..4925e4bc01d 100644 --- a/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts +++ b/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts @@ -1,12 +1,9 @@ -import { AsyncPipe } from "@angular/common"; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { Observable, Subject } from "rxjs"; -import { takeUntil } from "rxjs/operators"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { Observable } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { OrganizationId } from "@bitwarden/common/types/guid"; -import { AnchorLinkDirective, BannerComponent } from "@bitwarden/components"; -import { I18nPipe } from "@bitwarden/ui-common"; +import { BannerModule } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { OrganizationWarningsService } from "../services"; import { OrganizationFreeTrialWarning } from "../types"; @@ -37,33 +34,17 @@ import { OrganizationFreeTrialWarning } from "../types"; } `, - imports: [AnchorLinkDirective, AsyncPipe, BannerComponent, I18nPipe], + imports: [BannerModule, SharedModule], }) -export class OrganizationFreeTrialWarningComponent implements OnInit, OnDestroy { +export class OrganizationFreeTrialWarningComponent implements OnInit { @Input({ required: true }) organization!: Organization; @Output() clicked = new EventEmitter(); - warning$!: Observable; - private destroy$ = new Subject(); + warning$!: Observable; constructor(private organizationWarningsService: OrganizationWarningsService) {} ngOnInit() { this.warning$ = this.organizationWarningsService.getFreeTrialWarning$(this.organization); - this.organizationWarningsService - .refreshWarningsForOrganization$(this.organization.id as OrganizationId) - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.refresh(); - }); } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - refresh = () => { - this.warning$ = this.organizationWarningsService.getFreeTrialWarning$(this.organization, true); - }; } diff --git a/apps/web/src/app/billing/warnings/components/organization-reseller-renewal-warning.component.ts b/apps/web/src/app/billing/organizations/warnings/components/organization-reseller-renewal-warning.component.ts similarity index 82% rename from apps/web/src/app/billing/warnings/components/organization-reseller-renewal-warning.component.ts rename to apps/web/src/app/billing/organizations/warnings/components/organization-reseller-renewal-warning.component.ts index f45dd443dda..4eba9f3daf5 100644 --- a/apps/web/src/app/billing/warnings/components/organization-reseller-renewal-warning.component.ts +++ b/apps/web/src/app/billing/organizations/warnings/components/organization-reseller-renewal-warning.component.ts @@ -1,9 +1,9 @@ -import { AsyncPipe } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { Observable } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { BannerComponent } from "@bitwarden/components"; +import { BannerModule } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { OrganizationWarningsService } from "../services"; import { OrganizationResellerRenewalWarning } from "../types"; @@ -25,12 +25,12 @@ import { OrganizationResellerRenewalWarning } from "../types"; } `, - imports: [AsyncPipe, BannerComponent], + imports: [BannerModule, SharedModule], }) export class OrganizationResellerRenewalWarningComponent implements OnInit { @Input({ required: true }) organization!: Organization; - warning$!: Observable; + warning$!: Observable; constructor(private organizationWarningsService: OrganizationWarningsService) {} diff --git a/apps/web/src/app/billing/organizations/warnings/organization-warnings.module.ts b/apps/web/src/app/billing/organizations/warnings/organization-warnings.module.ts new file mode 100644 index 00000000000..6defee7e78b --- /dev/null +++ b/apps/web/src/app/billing/organizations/warnings/organization-warnings.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { + OrganizationBillingClient, + SubscriberBillingClient, +} from "@bitwarden/web-vault/app/billing/clients"; +import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; + +@NgModule({ + providers: [OrganizationBillingClient, OrganizationWarningsService, SubscriberBillingClient], +}) +export class OrganizationWarningsModule {} diff --git a/apps/web/src/app/billing/warnings/services/index.ts b/apps/web/src/app/billing/organizations/warnings/services/index.ts similarity index 100% rename from apps/web/src/app/billing/warnings/services/index.ts rename to apps/web/src/app/billing/organizations/warnings/services/index.ts diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts new file mode 100644 index 00000000000..c7a297cc28b --- /dev/null +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts @@ -0,0 +1,682 @@ +jest.mock("@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component", () => ({ + ChangePlanDialogResultType: { + Submitted: "submitted", + Cancelled: "cancelled", + }, + openChangePlanDialog: jest.fn(), +})); + +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.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 { DialogRef, DialogService } from "@bitwarden/components"; +import { OrganizationBillingClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component"; +import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services/organization-warnings.service"; +import { OrganizationWarningsResponse } from "@bitwarden/web-vault/app/billing/organizations/warnings/types"; +import { + TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, + TrialPaymentDialogComponent, + TrialPaymentDialogResultType, +} from "@bitwarden/web-vault/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component"; +import { TaxIdWarningTypes } from "@bitwarden/web-vault/app/billing/warnings/types"; + +describe("OrganizationWarningsService", () => { + let service: OrganizationWarningsService; + let configService: MockProxy; + let dialogService: MockProxy; + let i18nService: MockProxy; + let organizationApiService: MockProxy; + let organizationBillingClient: MockProxy; + let router: MockProxy; + + const organization = { + id: "org-id-123", + name: "Test Organization", + providerName: "Test Reseller Inc", + productTierType: ProductTierType.Enterprise, + } as Organization; + + const format = (date: Date): string => + date.toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + }); + + beforeEach(() => { + configService = mock(); + dialogService = mock(); + i18nService = mock(); + organizationApiService = mock(); + organizationBillingClient = mock(); + router = mock(); + + (openChangePlanDialog as jest.Mock).mockReset(); + + i18nService.t.mockImplementation((key: string, ...args: any[]) => { + switch (key) { + case "freeTrialEndPromptCount": + return `Your free trial ends in ${args[0]} days.`; + case "freeTrialEndPromptTomorrowNoOrgName": + return "Your free trial ends tomorrow."; + case "freeTrialEndingTodayWithoutOrgName": + return "Your free trial ends today."; + case "resellerRenewalWarningMsg": + return `Your subscription will renew soon. To ensure uninterrupted service, contact ${args[0]} to confirm your renewal before ${args[1]}.`; + case "resellerOpenInvoiceWarningMgs": + return `An invoice for your subscription was issued on ${args[1]}. To ensure uninterrupted service, contact ${args[0]} to confirm your renewal before ${args[2]}.`; + case "resellerPastDueWarningMsg": + return `The invoice for your subscription has not been paid. To ensure uninterrupted service, contact ${args[0]} to confirm your renewal before ${args[1]}.`; + case "suspendedOrganizationTitle": + return `${args[0]} subscription suspended`; + case "close": + return "Close"; + case "continue": + return "Continue"; + default: + return key; + } + }); + + TestBed.configureTestingModule({ + providers: [ + OrganizationWarningsService, + { provide: ConfigService, useValue: configService }, + { provide: DialogService, useValue: dialogService }, + { provide: I18nService, useValue: i18nService }, + { provide: OrganizationApiServiceAbstraction, useValue: organizationApiService }, + { provide: OrganizationBillingClient, useValue: organizationBillingClient }, + { provide: Router, useValue: router }, + ], + }); + + service = TestBed.inject(OrganizationWarningsService); + }); + + describe("getFreeTrialWarning$", () => { + it("should return null when no free trial warning exists", (done) => { + organizationBillingClient.getWarnings.mockResolvedValue({} as OrganizationWarningsResponse); + + service.getFreeTrialWarning$(organization).subscribe((result) => { + expect(result).toBeNull(); + done(); + }); + }); + + it("should return warning with count message when remaining trial days >= 2", (done) => { + const warning = { remainingTrialDays: 5 }; + organizationBillingClient.getWarnings.mockResolvedValue({ + freeTrial: warning, + } as OrganizationWarningsResponse); + + service.getFreeTrialWarning$(organization).subscribe((result) => { + expect(result).toEqual({ + organization: organization, + message: "Your free trial ends in 5 days.", + }); + expect(i18nService.t).toHaveBeenCalledWith("freeTrialEndPromptCount", 5); + done(); + }); + }); + + it("should return warning with tomorrow message when remaining trial days = 1", (done) => { + const warning = { remainingTrialDays: 1 }; + organizationBillingClient.getWarnings.mockResolvedValue({ + freeTrial: warning, + } as OrganizationWarningsResponse); + + service.getFreeTrialWarning$(organization).subscribe((result) => { + expect(result).toEqual({ + organization: organization, + message: "Your free trial ends tomorrow.", + }); + expect(i18nService.t).toHaveBeenCalledWith("freeTrialEndPromptTomorrowNoOrgName"); + done(); + }); + }); + + it("should return warning with today message when remaining trial days = 0", (done) => { + const warning = { remainingTrialDays: 0 }; + organizationBillingClient.getWarnings.mockResolvedValue({ + freeTrial: warning, + } as OrganizationWarningsResponse); + + service.getFreeTrialWarning$(organization).subscribe((result) => { + expect(result).toEqual({ + organization: organization, + message: "Your free trial ends today.", + }); + expect(i18nService.t).toHaveBeenCalledWith("freeTrialEndingTodayWithoutOrgName"); + done(); + }); + }); + + it("should refresh warning when refreshFreeTrialWarning is called", (done) => { + const initialWarning = { remainingTrialDays: 3 }; + const refreshedWarning = { remainingTrialDays: 2 }; + let invocationCount = 0; + + organizationBillingClient.getWarnings + .mockResolvedValueOnce({ + freeTrial: initialWarning, + } as OrganizationWarningsResponse) + .mockResolvedValueOnce({ + freeTrial: refreshedWarning, + } as OrganizationWarningsResponse); + + const subscription = service.getFreeTrialWarning$(organization).subscribe((result) => { + invocationCount++; + + if (invocationCount === 1) { + expect(result).toEqual({ + organization: organization, + message: "Your free trial ends in 3 days.", + }); + } else if (invocationCount === 2) { + expect(result).toEqual({ + organization: organization, + message: "Your free trial ends in 2 days.", + }); + subscription.unsubscribe(); + done(); + } + }); + + setTimeout(() => { + service.refreshFreeTrialWarning(); + }, 10); + }); + }); + + describe("getResellerRenewalWarning$", () => { + it("should return null when no reseller renewal warning exists", (done) => { + organizationBillingClient.getWarnings.mockResolvedValue({} as OrganizationWarningsResponse); + + service.getResellerRenewalWarning$(organization).subscribe((result) => { + expect(result).toBeNull(); + done(); + }); + }); + + it("should return upcoming warning with correct type and message", (done) => { + const renewalDate = new Date(2024, 11, 31); + const warning = { + type: "upcoming" as const, + upcoming: { renewalDate }, + }; + organizationBillingClient.getWarnings.mockResolvedValue({ + resellerRenewal: warning, + } as OrganizationWarningsResponse); + + service.getResellerRenewalWarning$(organization).subscribe((result) => { + const expectedFormattedDate = format(renewalDate); + + expect(result).toEqual({ + type: "info", + message: `Your subscription will renew soon. To ensure uninterrupted service, contact Test Reseller Inc to confirm your renewal before ${expectedFormattedDate}.`, + }); + expect(i18nService.t).toHaveBeenCalledWith( + "resellerRenewalWarningMsg", + "Test Reseller Inc", + expectedFormattedDate, + ); + done(); + }); + }); + + it("should return issued warning with correct type and message", (done) => { + const issuedDate = new Date(2024, 10, 15); + const dueDate = new Date(2024, 11, 15); + const warning = { + type: "issued" as const, + issued: { issuedDate, dueDate }, + }; + organizationBillingClient.getWarnings.mockResolvedValue({ + resellerRenewal: warning, + } as OrganizationWarningsResponse); + + service.getResellerRenewalWarning$(organization).subscribe((result) => { + const expectedIssuedDate = format(issuedDate); + const expectedDueDate = format(dueDate); + + expect(result).toEqual({ + type: "info", + message: `An invoice for your subscription was issued on ${expectedIssuedDate}. To ensure uninterrupted service, contact Test Reseller Inc to confirm your renewal before ${expectedDueDate}.`, + }); + expect(i18nService.t).toHaveBeenCalledWith( + "resellerOpenInvoiceWarningMgs", + "Test Reseller Inc", + expectedIssuedDate, + expectedDueDate, + ); + done(); + }); + }); + + it("should return past_due warning with correct type and message", (done) => { + const suspensionDate = new Date(2024, 11, 1); + const warning = { + type: "past_due" as const, + pastDue: { suspensionDate }, + }; + organizationBillingClient.getWarnings.mockResolvedValue({ + resellerRenewal: warning, + } as OrganizationWarningsResponse); + + service.getResellerRenewalWarning$(organization).subscribe((result) => { + const expectedSuspensionDate = format(suspensionDate); + + expect(result).toEqual({ + type: "warning", + message: `The invoice for your subscription has not been paid. To ensure uninterrupted service, contact Test Reseller Inc to confirm your renewal before ${expectedSuspensionDate}.`, + }); + expect(i18nService.t).toHaveBeenCalledWith( + "resellerPastDueWarningMsg", + "Test Reseller Inc", + expectedSuspensionDate, + ); + done(); + }); + }); + }); + + describe("getTaxIdWarning$", () => { + it("should return null when no tax ID warning exists", (done) => { + organizationBillingClient.getWarnings.mockResolvedValue({} as OrganizationWarningsResponse); + + service.getTaxIdWarning$(organization).subscribe((result) => { + expect(result).toBeNull(); + done(); + }); + }); + + it("should return tax_id_missing type when tax ID is missing", (done) => { + const warning = { type: TaxIdWarningTypes.Missing }; + organizationBillingClient.getWarnings.mockResolvedValue({ + taxId: warning, + } as OrganizationWarningsResponse); + + service.getTaxIdWarning$(organization).subscribe((result) => { + expect(result).toBe(TaxIdWarningTypes.Missing); + done(); + }); + }); + + it("should return tax_id_pending_verification type when tax ID verification is pending", (done) => { + const warning = { type: TaxIdWarningTypes.PendingVerification }; + organizationBillingClient.getWarnings.mockResolvedValue({ + taxId: warning, + } as OrganizationWarningsResponse); + + service.getTaxIdWarning$(organization).subscribe((result) => { + expect(result).toBe(TaxIdWarningTypes.PendingVerification); + done(); + }); + }); + + it("should return tax_id_failed_verification type when tax ID verification failed", (done) => { + const warning = { type: TaxIdWarningTypes.FailedVerification }; + organizationBillingClient.getWarnings.mockResolvedValue({ + taxId: warning, + } as OrganizationWarningsResponse); + + service.getTaxIdWarning$(organization).subscribe((result) => { + expect(result).toBe(TaxIdWarningTypes.FailedVerification); + done(); + }); + }); + + it("should refresh warning and update taxIdWarningRefreshedSubject when refreshTaxIdWarning is called", (done) => { + const initialWarning = { type: TaxIdWarningTypes.Missing }; + const refreshedWarning = { type: TaxIdWarningTypes.FailedVerification }; + let invocationCount = 0; + + organizationBillingClient.getWarnings + .mockResolvedValueOnce({ + taxId: initialWarning, + } as OrganizationWarningsResponse) + .mockResolvedValueOnce({ + taxId: refreshedWarning, + } as OrganizationWarningsResponse); + + const subscription = service.getTaxIdWarning$(organization).subscribe((result) => { + invocationCount++; + + if (invocationCount === 1) { + expect(result).toBe(TaxIdWarningTypes.Missing); + } else if (invocationCount === 2) { + expect(result).toBe(TaxIdWarningTypes.FailedVerification); + subscription.unsubscribe(); + done(); + } + }); + + setTimeout(() => { + service.refreshTaxIdWarning(); + }, 10); + }); + + it("should update taxIdWarningRefreshedSubject with warning type when refresh returns a warning", (done) => { + const refreshedWarning = { type: TaxIdWarningTypes.Missing }; + let refreshedCount = 0; + + organizationBillingClient.getWarnings + .mockResolvedValueOnce({} as OrganizationWarningsResponse) + .mockResolvedValueOnce({ + taxId: refreshedWarning, + } as OrganizationWarningsResponse); + + const taxIdSubscription = service.taxIdWarningRefreshed$.subscribe((refreshedType) => { + refreshedCount++; + if (refreshedCount === 2) { + expect(refreshedType).toBe(TaxIdWarningTypes.Missing); + taxIdSubscription.unsubscribe(); + done(); + } + }); + + service.getTaxIdWarning$(organization).subscribe(); + + setTimeout(() => { + service.refreshTaxIdWarning(); + }, 10); + }); + + it("should update taxIdWarningRefreshedSubject with null when refresh returns no warning", (done) => { + const initialWarning = { type: TaxIdWarningTypes.Missing }; + let refreshedCount = 0; + + organizationBillingClient.getWarnings + .mockResolvedValueOnce({ + taxId: initialWarning, + } as OrganizationWarningsResponse) + .mockResolvedValueOnce({} as OrganizationWarningsResponse); + + const taxIdSubscription = service.taxIdWarningRefreshed$.subscribe((refreshedType) => { + refreshedCount++; + if (refreshedCount === 2) { + expect(refreshedType).toBeNull(); + taxIdSubscription.unsubscribe(); + done(); + } + }); + + service.getTaxIdWarning$(organization).subscribe(); + + setTimeout(() => { + service.refreshTaxIdWarning(); + }, 10); + }); + }); + + describe("showInactiveSubscriptionDialog$", () => { + it("should not show dialog when no inactive subscription warning exists", (done) => { + organizationBillingClient.getWarnings.mockResolvedValue({} as OrganizationWarningsResponse); + + service.showInactiveSubscriptionDialog$(organization).subscribe({ + complete: () => { + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + done(); + }, + }); + }); + + it("should show contact provider dialog for contact_provider resolution", (done) => { + const warning = { resolution: "contact_provider" }; + organizationBillingClient.getWarnings.mockResolvedValue({ + inactiveSubscription: warning, + } as OrganizationWarningsResponse); + + dialogService.openSimpleDialog.mockResolvedValue(true); + + service.showInactiveSubscriptionDialog$(organization).subscribe({ + complete: () => { + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: "Test Organization subscription suspended", + content: { + key: "suspendedManagedOrgMessage", + placeholders: ["Test Reseller Inc"], + }, + type: "danger", + acceptButtonText: "Close", + cancelButtonText: null, + }); + done(); + }, + }); + }); + + it("should show add payment method dialog and navigate when confirmed", (done) => { + const warning = { resolution: "add_payment_method" }; + organizationBillingClient.getWarnings.mockResolvedValue({ + inactiveSubscription: warning, + } as OrganizationWarningsResponse); + + dialogService.openSimpleDialog.mockResolvedValue(true); + configService.getFeatureFlag.mockResolvedValue(false); + router.navigate.mockResolvedValue(true); + + service.showInactiveSubscriptionDialog$(organization).subscribe({ + complete: () => { + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: "Test Organization subscription suspended", + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: "Continue", + cancelButtonText: "Close", + }); + expect(configService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, + ); + expect(router.navigate).toHaveBeenCalledWith( + ["organizations", "org-id-123", "billing", "payment-method"], + { state: { launchPaymentModalAutomatically: true } }, + ); + done(); + }, + }); + }); + + it("should navigate to payment-details when feature flag is enabled", (done) => { + const warning = { resolution: "add_payment_method" }; + organizationBillingClient.getWarnings.mockResolvedValue({ + inactiveSubscription: warning, + } as OrganizationWarningsResponse); + + dialogService.openSimpleDialog.mockResolvedValue(true); + configService.getFeatureFlag.mockResolvedValue(true); + router.navigate.mockResolvedValue(true); + + service.showInactiveSubscriptionDialog$(organization).subscribe({ + complete: () => { + expect(router.navigate).toHaveBeenCalledWith( + ["organizations", "org-id-123", "billing", "payment-details"], + { state: { launchPaymentModalAutomatically: true } }, + ); + done(); + }, + }); + }); + + it("should not navigate when add payment method dialog is cancelled", (done) => { + const warning = { resolution: "add_payment_method" }; + organizationBillingClient.getWarnings.mockResolvedValue({ + inactiveSubscription: warning, + } as OrganizationWarningsResponse); + + dialogService.openSimpleDialog.mockResolvedValue(false); + + service.showInactiveSubscriptionDialog$(organization).subscribe({ + complete: () => { + expect(dialogService.openSimpleDialog).toHaveBeenCalled(); + expect(configService.getFeatureFlag).not.toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }, + }); + }); + + it("should open change plan dialog for resubscribe resolution", (done) => { + const warning = { resolution: "resubscribe" }; + const subscription = { id: "sub-123" } as OrganizationSubscriptionResponse; + + organizationBillingClient.getWarnings.mockResolvedValue({ + inactiveSubscription: warning, + } as OrganizationWarningsResponse); + + organizationApiService.getSubscription.mockResolvedValue(subscription); + + const mockDialogRef = { + closed: of("submitted"), + } as DialogRef; + + (openChangePlanDialog as jest.Mock).mockReturnValue(mockDialogRef); + + service.showInactiveSubscriptionDialog$(organization).subscribe({ + complete: () => { + expect(organizationApiService.getSubscription).toHaveBeenCalledWith(organization.id); + expect(openChangePlanDialog).toHaveBeenCalledWith(dialogService, { + data: { + organizationId: organization.id, + subscription: subscription, + productTierType: organization.productTierType, + }, + }); + done(); + }, + }); + }); + + it("should show contact owner dialog for contact_owner resolution", (done) => { + const warning = { resolution: "contact_owner" }; + organizationBillingClient.getWarnings.mockResolvedValue({ + inactiveSubscription: warning, + } as OrganizationWarningsResponse); + + dialogService.openSimpleDialog.mockResolvedValue(true); + + service.showInactiveSubscriptionDialog$(organization).subscribe({ + complete: () => { + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: "Test Organization subscription suspended", + content: { key: "suspendedUserOrgMessage" }, + type: "danger", + acceptButtonText: "Close", + cancelButtonText: null, + }); + done(); + }, + }); + }); + }); + + describe("showSubscribeBeforeFreeTrialEndsDialog$", () => { + it("should not show dialog when no free trial warning exists", (done) => { + organizationBillingClient.getWarnings.mockResolvedValue({} as OrganizationWarningsResponse); + + service.showSubscribeBeforeFreeTrialEndsDialog$(organization).subscribe({ + complete: () => { + expect(organizationApiService.getSubscription).not.toHaveBeenCalled(); + done(); + }, + }); + }); + + it("should open trial payment dialog when free trial warning exists", (done) => { + const warning = { remainingTrialDays: 2 }; + const subscription = { id: "sub-123" } as OrganizationSubscriptionResponse; + + organizationBillingClient.getWarnings.mockResolvedValue({ + freeTrial: warning, + } as OrganizationWarningsResponse); + + organizationApiService.getSubscription.mockResolvedValue(subscription); + + const mockDialogRef = { + closed: of(TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.CLOSED), + } as DialogRef; + + const openSpy = jest + .spyOn(TrialPaymentDialogComponent, "open") + .mockReturnValue(mockDialogRef); + + service.showSubscribeBeforeFreeTrialEndsDialog$(organization).subscribe({ + complete: () => { + expect(organizationApiService.getSubscription).toHaveBeenCalledWith(organization.id); + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + organizationId: organization.id, + subscription: subscription, + productTierType: organization.productTierType, + }, + }); + done(); + }, + }); + }); + + it("should refresh free trial warning when dialog result is SUBMITTED", (done) => { + const warning = { remainingTrialDays: 1 }; + const subscription = { id: "sub-456" } as OrganizationSubscriptionResponse; + + organizationBillingClient.getWarnings.mockResolvedValue({ + freeTrial: warning, + } as OrganizationWarningsResponse); + + organizationApiService.getSubscription.mockResolvedValue(subscription); + + const mockDialogRef = { + closed: of(TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED), + } as DialogRef; + + jest.spyOn(TrialPaymentDialogComponent, "open").mockReturnValue(mockDialogRef); + + const refreshTriggerSpy = jest.spyOn(service["refreshFreeTrialWarningTrigger"], "next"); + + service.showSubscribeBeforeFreeTrialEndsDialog$(organization).subscribe({ + complete: () => { + expect(refreshTriggerSpy).toHaveBeenCalled(); + done(); + }, + }); + }); + + it("should not refresh free trial warning when dialog result is CLOSED", (done) => { + const warning = { remainingTrialDays: 3 }; + const subscription = { id: "sub-789" } as OrganizationSubscriptionResponse; + + organizationBillingClient.getWarnings.mockResolvedValue({ + freeTrial: warning, + } as OrganizationWarningsResponse); + + organizationApiService.getSubscription.mockResolvedValue(subscription); + + const mockDialogRef = { + closed: of(TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.CLOSED), + } as DialogRef; + + jest.spyOn(TrialPaymentDialogComponent, "open").mockReturnValue(mockDialogRef); + const refreshSpy = jest.spyOn(service, "refreshFreeTrialWarning"); + + service.showSubscribeBeforeFreeTrialEndsDialog$(organization).subscribe({ + complete: () => { + expect(refreshSpy).not.toHaveBeenCalled(); + done(); + }, + }); + }); + }); +}); diff --git a/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts similarity index 63% rename from apps/web/src/app/billing/warnings/services/organization-warnings.service.ts rename to apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts index 78c17a5d384..5b466dfe41d 100644 --- a/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts @@ -1,26 +1,39 @@ -import { Location } from "@angular/common"; import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; -import { filter, from, lastValueFrom, map, Observable, Subject, switchMap, takeWhile } from "rxjs"; +import { + BehaviorSubject, + filter, + from, + lastValueFrom, + map, + merge, + Observable, + Subject, + switchMap, + tap, +} from "rxjs"; import { take } from "rxjs/operators"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; -import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.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 { SyncService } from "@bitwarden/common/platform/sync"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; +import { OrganizationBillingClient } from "@bitwarden/web-vault/app/billing/clients"; +import { TaxIdWarningType } from "@bitwarden/web-vault/app/billing/warnings/types"; -import { openChangePlanDialog } from "../../organizations/change-plan-dialog.component"; import { TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, TrialPaymentDialogComponent, -} from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; -import { OrganizationFreeTrialWarning, OrganizationResellerRenewalWarning } from "../types"; +} from "../../../shared/trial-payment-dialog/trial-payment-dialog.component"; +import { openChangePlanDialog } from "../../change-plan-dialog.component"; +import { + OrganizationFreeTrialWarning, + OrganizationResellerRenewalWarning, + OrganizationWarningsResponse, +} from "../types"; const format = (date: Date) => date.toLocaleDateString("en-US", { @@ -29,28 +42,39 @@ const format = (date: Date) => year: "numeric", }); -@Injectable({ providedIn: "root" }) +@Injectable() export class OrganizationWarningsService { private cache$ = new Map>(); - private refreshWarnings$ = new Subject(); + + private refreshFreeTrialWarningTrigger = new Subject(); + private refreshTaxIdWarningTrigger = new Subject(); + + private taxIdWarningRefreshedSubject = new BehaviorSubject(null); + taxIdWarningRefreshed$ = this.taxIdWarningRefreshedSubject.asObservable(); constructor( private configService: ConfigService, private dialogService: DialogService, private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, - private organizationBillingApiService: OrganizationBillingApiServiceAbstraction, + private organizationBillingClient: OrganizationBillingClient, private router: Router, - private location: Location, - protected syncService: SyncService, ) {} getFreeTrialWarning$ = ( organization: Organization, - bypassCache: boolean = false, - ): Observable => - this.getWarning$(organization, (response) => response.freeTrial, bypassCache).pipe( + ): Observable => + merge( + this.getWarning$(organization, (response) => response.freeTrial), + this.refreshFreeTrialWarningTrigger.pipe( + switchMap(() => this.getWarning$(organization, (response) => response.freeTrial, true)), + ), + ).pipe( map((warning) => { + if (!warning) { + return null; + } + const { remainingTrialDays } = warning; if (remainingTrialDays >= 2) { @@ -76,10 +100,12 @@ export class OrganizationWarningsService { getResellerRenewalWarning$ = ( organization: Organization, - bypassCache: boolean = false, - ): Observable => - this.getWarning$(organization, (response) => response.resellerRenewal, bypassCache).pipe( - map((warning): OrganizationResellerRenewalWarning | null => { + ): Observable => + this.getWarning$(organization, (response) => response.resellerRenewal).pipe( + map((warning) => { + if (!warning) { + return null; + } switch (warning.type) { case "upcoming": { return { @@ -114,14 +140,27 @@ export class OrganizationWarningsService { } } }), - filter((result): result is NonNullable => result !== null), ); - showInactiveSubscriptionDialog$ = ( - organization: Organization, - bypassCache: boolean = false, - ): Observable => - this.getWarning$(organization, (response) => response.inactiveSubscription, bypassCache).pipe( + getTaxIdWarning$ = (organization: Organization): Observable => + merge( + this.getWarning$(organization, (response) => response.taxId), + this.refreshTaxIdWarningTrigger.pipe( + switchMap(() => + this.getWarning$(organization, (response) => response.taxId, true).pipe( + tap((warning) => this.taxIdWarningRefreshedSubject.next(warning ? warning.type : null)), + ), + ), + ), + ).pipe(map((warning) => (warning ? warning.type : null))); + + refreshFreeTrialWarning = () => this.refreshFreeTrialWarningTrigger.next(); + + refreshTaxIdWarning = () => this.refreshTaxIdWarningTrigger.next(); + + showInactiveSubscriptionDialog$ = (organization: Organization): Observable => + this.getWarning$(organization, (response) => response.inactiveSubscription).pipe( + filter((warning) => warning !== null), switchMap(async (warning) => { switch (warning.resolution) { case "contact_provider": { @@ -183,43 +222,43 @@ export class OrganizationWarningsService { }); break; } - case "add_payment_method_optional_trial": { - const organizationSubscriptionResponse = - await this.organizationApiService.getSubscription(organization.id); - - const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { - data: { - organizationId: organization.id, - subscription: organizationSubscriptionResponse, - productTierType: organization?.productTierType, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { - this.refreshWarnings$.next(organization.id as OrganizationId); - } - } } }), ); - refreshWarningsForOrganization$(organizationId: OrganizationId): Observable { - return this.refreshWarnings$.pipe( - filter((id) => id === organizationId), - map((): void => void 0), - ); - } + showSubscribeBeforeFreeTrialEndsDialog$ = (organization: Organization): Observable => + this.getWarning$(organization, (response) => response.freeTrial).pipe( + filter((warning) => warning !== null), + switchMap(async () => { + const organizationSubscriptionResponse = await this.organizationApiService.getSubscription( + organization.id, + ); - private getResponse$ = ( + const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { + data: { + organizationId: organization.id, + subscription: organizationSubscriptionResponse, + productTierType: organization?.productTierType, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { + this.refreshFreeTrialWarningTrigger.next(); + } + }), + ); + + private readThroughWarnings$ = ( organization: Organization, bypassCache: boolean = false, ): Observable => { - const existing = this.cache$.get(organization.id as OrganizationId); + const organizationId = organization.id as OrganizationId; + const existing = this.cache$.get(organizationId); if (existing && !bypassCache) { return existing; } - const response$ = from(this.organizationBillingApiService.getWarnings(organization.id)); - this.cache$.set(organization.id as OrganizationId, response$); + const response$ = from(this.organizationBillingClient.getWarnings(organizationId)); + this.cache$.set(organizationId, response$); return response$; }; @@ -227,10 +266,12 @@ export class OrganizationWarningsService { organization: Organization, extract: (response: OrganizationWarningsResponse) => T | null | undefined, bypassCache: boolean = false, - ): Observable => - this.getResponse$(organization, bypassCache).pipe( - map(extract), - takeWhile((warning): warning is T => !!warning), + ): Observable => + this.readThroughWarnings$(organization, bypassCache).pipe( + map((response) => { + const value = extract(response); + return value ? value : null; + }), take(1), ); } diff --git a/apps/web/src/app/billing/organizations/warnings/types/index.ts b/apps/web/src/app/billing/organizations/warnings/types/index.ts new file mode 100644 index 00000000000..fc0c7d278ed --- /dev/null +++ b/apps/web/src/app/billing/organizations/warnings/types/index.ts @@ -0,0 +1 @@ +export * from "./organization-warnings"; diff --git a/libs/common/src/billing/models/response/organization-warnings.response.ts b/apps/web/src/app/billing/organizations/warnings/types/organization-warnings.ts similarity index 80% rename from libs/common/src/billing/models/response/organization-warnings.response.ts rename to apps/web/src/app/billing/organizations/warnings/types/organization-warnings.ts index ff70298101e..0c0097d5b09 100644 --- a/libs/common/src/billing/models/response/organization-warnings.response.ts +++ b/apps/web/src/app/billing/organizations/warnings/types/organization-warnings.ts @@ -1,9 +1,22 @@ -import { BaseResponse } from "../../../models/response/base.response"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { TaxIdWarningResponse } from "@bitwarden/web-vault/app/billing/warnings/types"; + +export type OrganizationFreeTrialWarning = { + organization: Pick; + message: string; +}; + +export type OrganizationResellerRenewalWarning = { + type: "info" | "warning"; + message: string; +}; export class OrganizationWarningsResponse extends BaseResponse { freeTrial?: FreeTrialWarningResponse; inactiveSubscription?: InactiveSubscriptionWarningResponse; resellerRenewal?: ResellerRenewalWarningResponse; + taxId?: TaxIdWarningResponse; constructor(response: any) { super(response); @@ -21,6 +34,10 @@ export class OrganizationWarningsResponse extends BaseResponse { if (resellerWarning) { this.resellerRenewal = new ResellerRenewalWarningResponse(resellerWarning); } + const taxIdWarning = this.getResponseProperty("TaxId"); + if (taxIdWarning) { + this.taxId = new TaxIdWarningResponse(taxIdWarning); + } } } diff --git a/apps/web/src/app/billing/payment/components/add-account-credit-dialog.component.ts b/apps/web/src/app/billing/payment/components/add-account-credit-dialog.component.ts index 2030d0e73ec..a83a00e8158 100644 --- a/apps/web/src/app/billing/payment/components/add-account-credit-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/add-account-credit-dialog.component.ts @@ -14,13 +14,13 @@ 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 { DialogConfig, DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; import { SharedModule } from "../../../shared"; -import { BillingClient } from "../../services"; -import { BillableEntity } from "../../types"; +import { BitwardenSubscriber } from "../../types"; type DialogParams = { - owner: BillableEntity; + subscriber: BitwardenSubscriber; }; type DialogResult = "cancelled" | "error" | "launched"; @@ -125,7 +125,7 @@ const positiveNumberValidator = `, standalone: true, imports: [SharedModule], - providers: [BillingClient], + providers: [SubscriberBillingClient], }) export class AddAccountCreditDialogComponent { @ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm!: ElementRef; @@ -143,22 +143,22 @@ export class AddAccountCreditDialogComponent { protected payPalCustom$ = this.configService.cloudRegion$.pipe( map((cloudRegion) => { - switch (this.dialogParams.owner.type) { + switch (this.dialogParams.subscriber.type) { case "account": { - return `user_id=${this.dialogParams.owner.data.id},account_credit=1,region=${cloudRegion}`; + return `user_id:${this.dialogParams.subscriber.data.id},account_credit:1,region:${cloudRegion}`; } case "organization": { - return `organization_id=${this.dialogParams.owner.data.id},account_credit=1,region=${cloudRegion}`; + return `organization_id:${this.dialogParams.subscriber.data.id},account_credit:1,region:${cloudRegion}`; } case "provider": { - return `provider_id=${this.dialogParams.owner.data.id},account_credit=1,region=${cloudRegion}`; + return `provider_id:${this.dialogParams.subscriber.data.id},account_credit:1,region:${cloudRegion}`; } } }), ); constructor( - private billingClient: BillingClient, + private billingClient: SubscriberBillingClient, private configService: ConfigService, @Inject(DIALOG_DATA) private dialogParams: DialogParams, private dialogRef: DialogRef, @@ -175,7 +175,7 @@ export class AddAccountCreditDialogComponent { } if (this.formGroup.value.paymentMethod === "bitPay") { - const result = await this.billingClient.addCreditWithBitPay(this.dialogParams.owner, { + const result = await this.billingClient.addCreditWithBitPay(this.dialogParams.subscriber, { amount: this.amount!, redirectUrl: this.redirectUrl, }); @@ -225,13 +225,13 @@ export class AddAccountCreditDialogComponent { } get payPalSubject(): string { - switch (this.dialogParams.owner.type) { + switch (this.dialogParams.subscriber.type) { case "account": { - return this.dialogParams.owner.data.email; + return this.dialogParams.subscriber.data.email; } case "organization": case "provider": { - return this.dialogParams.owner.data.name; + return this.dialogParams.subscriber.data.name; } } } diff --git a/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts b/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts index 15c63d8f99f..4d2fadaa894 100644 --- a/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts @@ -3,10 +3,10 @@ import { Component, Inject } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogConfig, DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; import { SharedModule } from "../../../shared"; -import { BillingClient } from "../../services"; -import { BillableEntity } from "../../types"; +import { BitwardenSubscriber } from "../../types"; import { EnterPaymentMethodComponent } from "./enter-payment-method.component"; import { @@ -15,7 +15,7 @@ import { } from "./submit-payment-method-dialog.component"; type DialogParams = { - owner: BillableEntity; + subscriber: BitwardenSubscriber; }; @Component({ @@ -28,7 +28,7 @@ type DialogParams = {
@@ -51,20 +51,20 @@ type DialogParams = { `, standalone: true, imports: [EnterPaymentMethodComponent, SharedModule], - providers: [BillingClient], + providers: [SubscriberBillingClient], }) export class ChangePaymentMethodDialogComponent extends SubmitPaymentMethodDialogComponent { - protected override owner: BillableEntity; + protected override subscriber: BitwardenSubscriber; constructor( - billingClient: BillingClient, + billingClient: SubscriberBillingClient, @Inject(DIALOG_DATA) protected dialogParams: DialogParams, dialogRef: DialogRef, i18nService: I18nService, toastService: ToastService, ) { super(billingClient, dialogRef, i18nService, toastService); - this.owner = this.dialogParams.owner; + this.subscriber = this.dialogParams.subscriber; } static open = (dialogService: DialogService, dialogConfig: DialogConfig) => diff --git a/apps/web/src/app/billing/payment/components/display-account-credit.component.ts b/apps/web/src/app/billing/payment/components/display-account-credit.component.ts index 7cbe3a27f30..f6aa0ef58bb 100644 --- a/apps/web/src/app/billing/payment/components/display-account-credit.component.ts +++ b/apps/web/src/app/billing/payment/components/display-account-credit.component.ts @@ -3,10 +3,10 @@ import { Component, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; import { SharedModule } from "../../../shared"; -import { BillingClient } from "../../services"; -import { BillableEntity } from "../../types"; +import { BitwardenSubscriber } from "../../types"; import { AddAccountCreditDialogComponent } from "./add-account-credit-dialog.component"; @@ -23,14 +23,14 @@ import { AddAccountCreditDialogComponent } from "./add-account-credit-dialog.com `, standalone: true, imports: [SharedModule], - providers: [BillingClient, CurrencyPipe], + providers: [SubscriberBillingClient, CurrencyPipe], }) export class DisplayAccountCreditComponent { - @Input({ required: true }) owner!: BillableEntity; + @Input({ required: true }) subscriber!: BitwardenSubscriber; @Input({ required: true }) credit!: number | null; constructor( - private billingClient: BillingClient, + private billingClient: SubscriberBillingClient, private currencyPipe: CurrencyPipe, private dialogService: DialogService, private i18nService: I18nService, @@ -38,8 +38,8 @@ export class DisplayAccountCreditComponent { ) {} addAccountCredit = async () => { - if (this.owner.type !== "account") { - const billingAddress = await this.billingClient.getBillingAddress(this.owner); + if (this.subscriber.type !== "account") { + const billingAddress = await this.billingClient.getBillingAddress(this.subscriber); if (!billingAddress) { this.toastService.showToast({ variant: "error", @@ -51,7 +51,7 @@ export class DisplayAccountCreditComponent { AddAccountCreditDialogComponent.open(this.dialogService, { data: { - owner: this.owner, + subscriber: this.subscriber, }, }); }; diff --git a/apps/web/src/app/billing/payment/components/display-billing-address.component.ts b/apps/web/src/app/billing/payment/components/display-billing-address.component.ts index f0a11321e5d..03d21a79003 100644 --- a/apps/web/src/app/billing/payment/components/display-billing-address.component.ts +++ b/apps/web/src/app/billing/payment/components/display-billing-address.component.ts @@ -2,23 +2,38 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { lastValueFrom } from "rxjs"; import { DialogService } from "@bitwarden/components"; - -import { SharedModule } from "../../../shared"; -import { BillableEntity } from "../../types"; -import { AddressPipe } from "../pipes"; -import { BillingAddress } from "../types"; - -import { EditBillingAddressDialogComponent } from "./edit-billing-address-dialog.component"; +import { EditBillingAddressDialogComponent } from "@bitwarden/web-vault/app/billing/payment/components/edit-billing-address-dialog.component"; +import { AddressPipe } from "@bitwarden/web-vault/app/billing/payment/pipes"; +import { BillingAddress } from "@bitwarden/web-vault/app/billing/payment/types"; +import { BitwardenSubscriber } from "@bitwarden/web-vault/app/billing/types"; +import { + TaxIdWarningType, + TaxIdWarningTypes, +} from "@bitwarden/web-vault/app/billing/warnings/types"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; @Component({ selector: "app-display-billing-address", template: ` -

{{ "billingAddress" | i18n }}

+

+ {{ "billingAddress" | i18n }} + @if (showMissingTaxIdBadge) { + {{ "missingTaxId" | i18n }} + } +

@if (billingAddress) {

{{ billingAddress | address }}

@if (billingAddress.taxId) { -

{{ "taxId" | i18n: billingAddress.taxId.value }}

+

+ {{ "taxId" | i18n: billingAddress.taxId.value }} + @if (showTaxIdPendingVerificationBadge) { + {{ "pendingVerification" | i18n }} + } + @if (showUnverifiedTaxIdBadge) { + {{ "unverified" | i18n }} + } +

} } @else {

{{ "noBillingAddress" | i18n }}

@@ -33,8 +48,9 @@ import { EditBillingAddressDialogComponent } from "./edit-billing-address-dialog imports: [AddressPipe, SharedModule], }) export class DisplayBillingAddressComponent { - @Input({ required: true }) owner!: BillableEntity; + @Input({ required: true }) subscriber!: BitwardenSubscriber; @Input({ required: true }) billingAddress!: BillingAddress | null; + @Input() taxIdWarning?: TaxIdWarningType; @Output() updated = new EventEmitter(); constructor(private dialogService: DialogService) {} @@ -42,8 +58,9 @@ export class DisplayBillingAddressComponent { editBillingAddress = async (): Promise => { const dialogRef = EditBillingAddressDialogComponent.open(this.dialogService, { data: { - owner: this.owner, + subscriber: this.subscriber, billingAddress: this.billingAddress, + taxIdWarning: this.taxIdWarning, }, }); @@ -53,4 +70,22 @@ export class DisplayBillingAddressComponent { this.updated.emit(result.billingAddress); } }; + + get showMissingTaxIdBadge(): boolean { + return this.subscriber.type !== "account" && this.taxIdWarning === TaxIdWarningTypes.Missing; + } + + get showTaxIdPendingVerificationBadge(): boolean { + return ( + this.subscriber.type !== "account" && + this.taxIdWarning === TaxIdWarningTypes.PendingVerification + ); + } + + get showUnverifiedTaxIdBadge(): boolean { + return ( + this.subscriber.type !== "account" && + this.taxIdWarning === TaxIdWarningTypes.FailedVerification + ); + } } diff --git a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts index 769472bcfcf..df42d04b802 100644 --- a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts @@ -4,7 +4,7 @@ import { lastValueFrom } from "rxjs"; import { DialogService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; -import { BillableEntity } from "../../types"; +import { BitwardenSubscriber } from "../../types"; import { MaskedPaymentMethod } from "../types"; import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dialog.component"; @@ -19,7 +19,10 @@ import { VerifyBankAccountComponent } from "./verify-bank-account.component"; @switch (paymentMethod.type) { @case ("bankAccount") { @if (!paymentMethod.verified) { - + } @@ -63,7 +66,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account.component"; imports: [SharedModule, VerifyBankAccountComponent], }) export class DisplayPaymentMethodComponent { - @Input({ required: true }) owner!: BillableEntity; + @Input({ required: true }) subscriber!: BitwardenSubscriber; @Input({ required: true }) paymentMethod!: MaskedPaymentMethod | null; @Output() updated = new EventEmitter(); @@ -82,7 +85,7 @@ export class DisplayPaymentMethodComponent { changePaymentMethod = async (): Promise => { const dialogRef = ChangePaymentMethodDialogComponent.open(this.dialogService, { data: { - owner: this.owner, + subscriber: this.subscriber, }, }); diff --git a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts index c844d08df58..de2f2f94497 100644 --- a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts @@ -3,18 +3,31 @@ import { Component, Inject } from "@angular/core"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogConfig, DialogRef, DialogService, ToastService } from "@bitwarden/components"; - -import { SharedModule } from "../../../shared"; -import { BillingClient } from "../../services"; -import { BillableEntity } from "../../types"; -import { BillingAddress, getTaxIdTypeForCountry } from "../types"; +import { + CalloutTypes, + DialogConfig, + DialogRef, + DialogService, + ToastService, +} from "@bitwarden/components"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + BillingAddress, + getTaxIdTypeForCountry, +} from "@bitwarden/web-vault/app/billing/payment/types"; +import { BitwardenSubscriber } from "@bitwarden/web-vault/app/billing/types"; +import { + TaxIdWarningType, + TaxIdWarningTypes, +} from "@bitwarden/web-vault/app/billing/warnings/types"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { EnterBillingAddressComponent } from "./enter-billing-address.component"; type DialogParams = { - owner: BillableEntity; + subscriber: BitwardenSubscriber; billingAddress: BillingAddress | null; + taxIdWarning?: TaxIdWarningType; }; type DialogResult = @@ -30,11 +43,18 @@ type DialogResult = {{ "editBillingAddress" | i18n }}
+ @let callout = taxIdWarningCallout; + @if (callout) { + + {{ callout.message }} + + } @@ -57,13 +77,13 @@ type DialogResult = `, standalone: true, imports: [EnterBillingAddressComponent, SharedModule], - providers: [BillingClient], + providers: [SubscriberBillingClient], }) export class EditBillingAddressDialogComponent { protected formGroup = EnterBillingAddressComponent.getFormGroup(); constructor( - private billingClient: BillingClient, + private billingClient: SubscriberBillingClient, @Inject(DIALOG_DATA) protected dialogParams: DialogParams, private dialogRef: DialogRef, private i18nService: I18nService, @@ -93,7 +113,7 @@ export class EditBillingAddressDialogComponent { : { ...addressFields, taxId: null }; const result = await this.billingClient.updateBillingAddress( - this.dialogParams.owner, + this.dialogParams.subscriber, billingAddress, ); @@ -125,7 +145,7 @@ export class EditBillingAddressDialogComponent { }; get supportsTaxId(): boolean { - switch (this.dialogParams.owner.type) { + switch (this.dialogParams.subscriber.type) { case "account": { return false; } @@ -134,7 +154,7 @@ export class EditBillingAddressDialogComponent { ProductTierType.TeamsStarter, ProductTierType.Teams, ProductTierType.Enterprise, - ].includes(this.dialogParams.owner.data.productTierType); + ].includes(this.dialogParams.subscriber.data.productTierType); } case "provider": { return true; @@ -142,6 +162,37 @@ export class EditBillingAddressDialogComponent { } } + get taxIdWarningCallout(): { + type: CalloutTypes; + title: string; + message: string; + } | null { + if ( + !this.supportsTaxId || + !this.dialogParams.taxIdWarning || + this.dialogParams.taxIdWarning === TaxIdWarningTypes.PendingVerification + ) { + return null; + } + + switch (this.dialogParams.taxIdWarning) { + case TaxIdWarningTypes.Missing: { + return { + type: "warning", + title: this.i18nService.t("missingTaxIdCalloutTitle"), + message: this.i18nService.t("missingTaxIdCalloutDescription"), + }; + } + case TaxIdWarningTypes.FailedVerification: { + return { + type: "warning", + title: this.i18nService.t("unverifiedTaxIdCalloutTitle"), + message: this.i18nService.t("unverifiedTaxIdCalloutDescription"), + }; + } + } + } + static open = (dialogService: DialogService, dialogConfig: DialogConfig) => dialogService.open(EditBillingAddressDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts index ab59e965b4e..7659b7ed5ca 100644 --- a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts @@ -3,9 +3,14 @@ import { FormControl, FormGroup, Validators } from "@angular/forms"; import { map, Observable, startWith, Subject, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + TaxIdWarningType, + TaxIdWarningTypes, +} from "@bitwarden/web-vault/app/billing/warnings/types"; import { SharedModule } from "../../../shared"; -import { BillingAddress, selectableCountries, taxIdTypes } from "../types"; +import { BillingAddress, getTaxIdTypeForCountry, selectableCountries, taxIdTypes } from "../types"; export interface BillingAddressControls { country: string; @@ -28,6 +33,7 @@ type Scenario = type: "update"; existing?: BillingAddress; supportsTaxId: boolean; + taxIdWarning?: TaxIdWarningType; }; @Component({ @@ -110,7 +116,7 @@ type Scenario =
@if (supportsTaxId$ | async) { -
+
{{ "taxIdNumber" | i18n }} + @let hint = taxIdWarningHint; + @if (hint) { + {{ hint }} + }
} @@ -137,6 +154,8 @@ export class EnterBillingAddressComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); + constructor(private i18nService: I18nService) {} + ngOnInit() { switch (this.scenario.type) { case "checkout": { @@ -185,6 +204,40 @@ export class EnterBillingAddressComponent implements OnInit, OnDestroy { this.group.controls.state.disable(); }; + get taxIdWarningHint() { + if ( + this.scenario.type === "checkout" || + !this.scenario.supportsTaxId || + !this.group.value.country || + this.scenario.taxIdWarning !== TaxIdWarningTypes.FailedVerification + ) { + return null; + } + + const taxIdType = getTaxIdTypeForCountry(this.group.value.country); + + if (!taxIdType) { + return null; + } + + const checkInputFormat = this.i18nService.t("checkInputFormat"); + + switch (taxIdType.code) { + case "au_abn": { + const exampleFormat = this.i18nService.t("exampleTaxIdFormat", "ABN", taxIdType.example); + return `${checkInputFormat} ${exampleFormat}`; + } + case "eu_vat": { + const exampleFormat = this.i18nService.t("exampleTaxIdFormat", "EU VAT", taxIdType.example); + return `${checkInputFormat} ${exampleFormat}`; + } + case "gb_vat": { + const exampleFormat = this.i18nService.t("exampleTaxIdFormat", "GB VAT", taxIdType.example); + return `${checkInputFormat} ${exampleFormat}`; + } + } + } + static getFormGroup = (): BillingAddressFormGroup => new FormGroup({ country: new FormControl("", { diff --git a/apps/web/src/app/billing/payment/components/require-payment-method-dialog.component.ts b/apps/web/src/app/billing/payment/components/require-payment-method-dialog.component.ts index 72585badca0..b1ca1922775 100644 --- a/apps/web/src/app/billing/payment/components/require-payment-method-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/require-payment-method-dialog.component.ts @@ -9,10 +9,10 @@ import { DialogService, ToastService, } from "@bitwarden/components"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; import { SharedModule } from "../../../shared"; -import { BillingClient } from "../../services"; -import { BillableEntity } from "../../types"; +import { BitwardenSubscriber } from "../../types"; import { EnterPaymentMethodComponent } from "./enter-payment-method.component"; import { @@ -21,7 +21,7 @@ import { } from "./submit-payment-method-dialog.component"; type DialogParams = { - owner: BillableEntity; + subscriber: BitwardenSubscriber; callout: { type: CalloutTypes; title: string; @@ -53,20 +53,20 @@ type DialogParams = { `, standalone: true, imports: [EnterPaymentMethodComponent, SharedModule], - providers: [BillingClient], + providers: [SubscriberBillingClient], }) export class RequirePaymentMethodDialogComponent extends SubmitPaymentMethodDialogComponent { - protected override owner: BillableEntity; + protected override subscriber: BitwardenSubscriber; constructor( - billingClient: BillingClient, + billingClient: SubscriberBillingClient, @Inject(DIALOG_DATA) protected dialogParams: DialogParams, dialogRef: DialogRef, i18nService: I18nService, toastService: ToastService, ) { super(billingClient, dialogRef, i18nService, toastService); - this.owner = this.dialogParams.owner; + this.subscriber = this.dialogParams.subscriber; } static open = (dialogService: DialogService, dialogConfig: DialogConfig) => diff --git a/apps/web/src/app/billing/payment/components/submit-payment-method-dialog.component.ts b/apps/web/src/app/billing/payment/components/submit-payment-method-dialog.component.ts index 0a0a5bf26d9..62d2b775eb5 100644 --- a/apps/web/src/app/billing/payment/components/submit-payment-method-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/submit-payment-method-dialog.component.ts @@ -2,9 +2,9 @@ import { Component, ViewChild } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogRef, ToastService } from "@bitwarden/components"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; -import { BillingClient } from "../../services"; -import { BillableEntity } from "../../types"; +import { BitwardenSubscriber } from "../../types"; import { MaskedPaymentMethod } from "../types"; import { EnterPaymentMethodComponent } from "./enter-payment-method.component"; @@ -20,10 +20,10 @@ export abstract class SubmitPaymentMethodDialogComponent { private enterPaymentMethodComponent!: EnterPaymentMethodComponent; protected formGroup = EnterPaymentMethodComponent.getFormGroup(); - protected abstract owner: BillableEntity; + protected abstract subscriber: BitwardenSubscriber; protected constructor( - protected billingClient: BillingClient, + protected billingClient: SubscriberBillingClient, protected dialogRef: DialogRef, protected i18nService: I18nService, protected toastService: ToastService, @@ -43,7 +43,7 @@ export abstract class SubmitPaymentMethodDialogComponent { : null; const result = await this.billingClient.updatePaymentMethod( - this.owner, + this.subscriber, paymentMethod, billingAddress, ); diff --git a/apps/web/src/app/billing/payment/components/verify-bank-account.component.ts b/apps/web/src/app/billing/payment/components/verify-bank-account.component.ts index f79e9a1b5fc..b1a2814daf2 100644 --- a/apps/web/src/app/billing/payment/components/verify-bank-account.component.ts +++ b/apps/web/src/app/billing/payment/components/verify-bank-account.component.ts @@ -3,10 +3,10 @@ import { FormControl, FormGroup, Validators } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; import { SharedModule } from "../../../shared"; -import { BillingClient } from "../../services"; -import { BillableEntity } from "../../types"; +import { BitwardenSubscriber } from "../../types"; import { MaskedPaymentMethod } from "../types"; @Component({ @@ -32,10 +32,10 @@ import { MaskedPaymentMethod } from "../types"; `, standalone: true, imports: [SharedModule], - providers: [BillingClient], + providers: [SubscriberBillingClient], }) export class VerifyBankAccountComponent { - @Input({ required: true }) owner!: BillableEntity; + @Input({ required: true }) subscriber!: BitwardenSubscriber; @Output() verified = new EventEmitter(); protected formGroup = new FormGroup({ @@ -47,7 +47,7 @@ export class VerifyBankAccountComponent { }); constructor( - private billingClient: BillingClient, + private billingClient: SubscriberBillingClient, private i18nService: I18nService, private toastService: ToastService, ) {} @@ -60,7 +60,7 @@ export class VerifyBankAccountComponent { } const result = await this.billingClient.verifyBankAccount( - this.owner, + this.subscriber, this.formGroup.value.descriptorCode!, ); diff --git a/apps/web/src/app/billing/services/index.ts b/apps/web/src/app/billing/services/index.ts index dcd2c05034a..e291ca6a454 100644 --- a/apps/web/src/app/billing/services/index.ts +++ b/apps/web/src/app/billing/services/index.ts @@ -1,4 +1,3 @@ -export * from "./billing.client"; export * from "./billing-services.module"; export * from "./braintree.service"; export * from "./stripe.service"; diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.html b/apps/web/src/app/billing/settings/sponsoring-org-row.component.html index 1e5690cd85a..5167c0a5c32 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.html +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.html @@ -12,7 +12,7 @@ bitIconButton="bwi-ellipsis-v" buttonType="main" [bitMenuTriggerFor]="appListDropdown" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" >

diff --git a/apps/web/src/app/billing/types/billable-entity.ts b/apps/web/src/app/billing/types/bitwarden-subscriber.ts similarity index 67% rename from apps/web/src/app/billing/types/billable-entity.ts rename to apps/web/src/app/billing/types/bitwarden-subscriber.ts index 79ed12a4161..3454d6a9651 100644 --- a/apps/web/src/app/billing/types/billable-entity.ts +++ b/apps/web/src/app/billing/types/bitwarden-subscriber.ts @@ -4,12 +4,14 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -export type BillableEntity = +export type BitwardenSubscriber = | { type: "account"; data: Account } | { type: "organization"; data: Organization } | { type: "provider"; data: Provider }; -export const accountToBillableEntity = map((account) => { +export type NonIndividualSubscriber = Exclude; + +export const mapAccountToSubscriber = map((account) => { if (!account) { throw new Error("Account not found"); } @@ -19,7 +21,7 @@ export const accountToBillableEntity = map((acco }; }); -export const organizationToBillableEntity = map( +export const mapOrganizationToSubscriber = map( (organization) => { if (!organization) { throw new Error("Organization not found"); @@ -31,7 +33,7 @@ export const organizationToBillableEntity = map((provider) => { +export const mapProviderToSubscriber = map((provider) => { if (!provider) { throw new Error("Organization not found"); } diff --git a/apps/web/src/app/billing/types/index.ts b/apps/web/src/app/billing/types/index.ts index 1278e0f2e14..50c007677f3 100644 --- a/apps/web/src/app/billing/types/index.ts +++ b/apps/web/src/app/billing/types/index.ts @@ -1,2 +1,2 @@ -export * from "./billable-entity"; +export * from "./bitwarden-subscriber"; export * from "./free-trial"; diff --git a/apps/web/src/app/billing/warnings/components/index.ts b/apps/web/src/app/billing/warnings/components/index.ts index 1e1e0682e62..5edefadb1ee 100644 --- a/apps/web/src/app/billing/warnings/components/index.ts +++ b/apps/web/src/app/billing/warnings/components/index.ts @@ -1,2 +1 @@ -export * from "./organization-free-trial-warning.component"; -export * from "./organization-reseller-renewal-warning.component"; +export * from "./tax-id-warning.component"; diff --git a/apps/web/src/app/billing/warnings/components/tax-id-warning.component.ts b/apps/web/src/app/billing/warnings/components/tax-id-warning.component.ts new file mode 100644 index 00000000000..7527ef8f0b7 --- /dev/null +++ b/apps/web/src/app/billing/warnings/components/tax-id-warning.component.ts @@ -0,0 +1,286 @@ +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { + BehaviorSubject, + combineLatest, + filter, + firstValueFrom, + lastValueFrom, + map, + Observable, + switchMap, +} from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { BannerModule, DialogService } from "@bitwarden/components"; +import { BILLING_DISK, StateProvider, UserKeyDefinition } from "@bitwarden/state"; +import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; +import { EditBillingAddressDialogComponent } from "@bitwarden/web-vault/app/billing/payment/components"; +import { NonIndividualSubscriber } from "@bitwarden/web-vault/app/billing/types"; +import { + TaxIdWarningType, + TaxIdWarningTypes, +} from "@bitwarden/web-vault/app/billing/warnings/types"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +type DismissalCounts = { + [TaxIdWarningTypes.Missing]?: number; + [TaxIdWarningTypes.FailedVerification]?: number; +}; + +const DISMISSALS_COUNT_KEY = new UserKeyDefinition( + BILLING_DISK, + "taxIdWarningDismissalCounts", + { + deserializer: (dismissalCounts) => dismissalCounts, + clearOn: [], + }, +); + +type DismissedThisSession = { + [TaxIdWarningTypes.Missing]?: boolean; + [TaxIdWarningTypes.FailedVerification]?: boolean; +}; + +const DISMISSED_THIS_SESSION_KEY = new UserKeyDefinition( + BILLING_DISK, + "taxIdWarningDismissedThisSession", + { + deserializer: (dismissedThisSession) => dismissedThisSession, + clearOn: ["logout"], + }, +); + +type Dismissals = { + [TaxIdWarningTypes.Missing]: { + count: number; + dismissedThisSession: boolean; + }; + [TaxIdWarningTypes.FailedVerification]: { + count: number; + dismissedThisSession: boolean; + }; +}; + +const shouldShowWarning = ( + warning: Exclude, + dismissals: Dismissals, +) => { + const dismissalsForType = dismissals[warning]; + if (dismissalsForType.dismissedThisSession) { + return false; + } + return dismissalsForType.count < 3; +}; + +type View = { + message: string; + callToAction: string; +}; + +type GetWarning$ = () => Observable; + +@Component({ + selector: "app-tax-id-warning", + template: ` + @if (enableTaxIdWarning$ | async) { + @let view = view$ | async; + + @if (view) { + + {{ view.message }} + + {{ view.callToAction }} + + + } + } + `, + imports: [BannerModule, SharedModule], +}) +export class TaxIdWarningComponent implements OnInit { + @Input({ required: true }) subscriber!: NonIndividualSubscriber; + @Input({ required: true }) getWarning$!: GetWarning$; + @Output() billingAddressUpdated = new EventEmitter(); + + protected enableTaxIdWarning$ = this.configService.getFeatureFlag$( + FeatureFlag.PM22415_TaxIDWarnings, + ); + + protected userId$ = this.accountService.activeAccount$.pipe( + filter((account): account is Account => account !== null), + getUserId, + ); + + protected dismissals$: Observable = this.userId$.pipe( + switchMap((userId) => + combineLatest([ + this.stateProvider.getUser(userId, DISMISSALS_COUNT_KEY).state$.pipe( + map((dismissalCounts) => { + if (!dismissalCounts) { + return { + [TaxIdWarningTypes.Missing]: 0, + [TaxIdWarningTypes.FailedVerification]: 0, + }; + } + return { + [TaxIdWarningTypes.Missing]: dismissalCounts[TaxIdWarningTypes.Missing] ?? 0, + [TaxIdWarningTypes.FailedVerification]: + dismissalCounts[TaxIdWarningTypes.FailedVerification] ?? 0, + }; + }), + ), + this.stateProvider.getUser(userId, DISMISSED_THIS_SESSION_KEY).state$.pipe( + map((dismissedThisSession) => { + if (!dismissedThisSession) { + return { + [TaxIdWarningTypes.Missing]: false, + [TaxIdWarningTypes.FailedVerification]: false, + }; + } + return { + [TaxIdWarningTypes.Missing]: dismissedThisSession[TaxIdWarningTypes.Missing] ?? false, + [TaxIdWarningTypes.FailedVerification]: + dismissedThisSession[TaxIdWarningTypes.FailedVerification] ?? false, + }; + }), + ), + ]), + ), + map(([dismissalCounts, dismissedThisSession]) => ({ + [TaxIdWarningTypes.Missing]: { + count: dismissalCounts[TaxIdWarningTypes.Missing], + dismissedThisSession: dismissedThisSession[TaxIdWarningTypes.Missing], + }, + [TaxIdWarningTypes.FailedVerification]: { + count: dismissalCounts[TaxIdWarningTypes.FailedVerification], + dismissedThisSession: dismissedThisSession[TaxIdWarningTypes.FailedVerification], + }, + })), + ); + + protected getWarningSubject = new BehaviorSubject(null); + + protected warning$ = this.getWarningSubject.pipe(switchMap(() => this.getWarning$())); + + protected view$: Observable = combineLatest([this.warning$, this.dismissals$]).pipe( + map(([warning, dismissals]) => { + if (!warning || warning === TaxIdWarningTypes.PendingVerification) { + return null; + } + + if (!shouldShowWarning(warning, dismissals)) { + return null; + } + + switch (warning) { + case TaxIdWarningTypes.Missing: { + return { + message: this.i18nService.t("missingTaxIdWarning"), + callToAction: this.i18nService.t("addTaxId"), + }; + } + case TaxIdWarningTypes.FailedVerification: { + return { + message: this.i18nService.t("unverifiedTaxIdWarning"), + callToAction: this.i18nService.t("editTaxId"), + }; + } + } + }), + ); + + constructor( + private accountService: AccountService, + private configService: ConfigService, + private dialogService: DialogService, + private i18nService: I18nService, + private subscriberBillingClient: SubscriberBillingClient, + private stateProvider: StateProvider, + ) {} + + ngOnInit() { + this.getWarningSubject.next(this.getWarning$); + } + + editBillingAddress = async () => { + const billingAddress = await this.subscriberBillingClient.getBillingAddress(this.subscriber); + const warning = (await firstValueFrom(this.warning$)) ?? undefined; + + const dialogRef = EditBillingAddressDialogComponent.open(this.dialogService, { + data: { + subscriber: this.subscriber, + billingAddress, + taxIdWarning: warning, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result?.type === "success") { + this.billingAddressUpdated.emit(); + } + }; + + trackDismissal = async () => { + const warning = await firstValueFrom(this.warning$); + if (!warning || warning === TaxIdWarningTypes.PendingVerification) { + return; + } + const userId = await firstValueFrom(this.userId$); + const updateDismissalCounts = this.stateProvider + .getUser(userId, DISMISSALS_COUNT_KEY) + .update((dismissalCounts) => { + if (!dismissalCounts) { + return { + [warning]: 1, + }; + } + const dismissalsByType = dismissalCounts[warning]; + if (!dismissalsByType) { + return { + ...dismissalCounts, + [warning]: 1, + }; + } + return { + ...dismissalCounts, + [warning]: dismissalsByType + 1, + }; + }); + const updateDismissedThisSession = this.stateProvider + .getUser(userId, DISMISSED_THIS_SESSION_KEY) + .update((dismissedThisSession) => { + if (!dismissedThisSession) { + return { + [warning]: true, + }; + } + const dismissedThisSessionByType = dismissedThisSession[warning]; + if (!dismissedThisSessionByType) { + return { + ...dismissedThisSession, + }; + } + return { + ...dismissedThisSession, + [warning]: dismissedThisSessionByType, + }; + }); + await Promise.all([updateDismissalCounts, updateDismissedThisSession]); + }; +} diff --git a/apps/web/src/app/billing/warnings/services/organization-warnings.service.spec.ts b/apps/web/src/app/billing/warnings/services/organization-warnings.service.spec.ts deleted file mode 100644 index c75dde0c9e5..00000000000 --- a/apps/web/src/app/billing/warnings/services/organization-warnings.service.spec.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { Router } from "@angular/router"; -import { mock, MockProxy } from "jest-mock-extended"; -import { firstValueFrom, lastValueFrom } from "rxjs"; - -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; -import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; - -import { OrganizationWarningsService } from "./organization-warnings.service"; - -// Skipped since Angular complains about `TypeError: Cannot read properties of undefined (reading 'ngModule')` -// which is typically a sign of circular dependencies. The problem seems to be originating from `ChangePlanDialogComponent`. -describe.skip("OrganizationWarningsService", () => { - let dialogService: MockProxy; - let i18nService: MockProxy; - let organizationApiService: MockProxy; - let organizationBillingApiService: MockProxy; - let router: MockProxy; - - let organizationWarningsService: OrganizationWarningsService; - - const respond = (responseBody: any) => - Promise.resolve(new OrganizationWarningsResponse(responseBody)); - - const empty = () => Promise.resolve(new OrganizationWarningsResponse({})); - - beforeEach(() => { - dialogService = mock(); - i18nService = mock(); - organizationApiService = mock(); - organizationBillingApiService = mock(); - router = mock(); - - organizationWarningsService = new OrganizationWarningsService( - dialogService, - i18nService, - organizationApiService, - organizationBillingApiService, - router, - ); - }); - - describe("cache$", () => { - it("should only request warnings once for a specific organization and replay the cached result for multiple subscriptions", async () => { - const response1 = respond({ - freeTrial: { - remainingTrialDays: 1, - }, - }); - - const organization1 = { - id: "1", - name: "Test", - } as Organization; - - const response2 = respond({ - freeTrial: { - remainingTrialDays: 2, - }, - }); - - const organization2 = { - id: "2", - name: "Test", - } as Organization; - - organizationBillingApiService.getWarnings.mockImplementation((id) => { - if (id === organization1.id) { - return response1; - } - - if (id === organization2.id) { - return response2; - } - - return empty(); - }); - - const oneDayRemainingTranslation = "oneDayRemaining"; - const twoDaysRemainingTranslation = "twoDaysRemaining"; - - i18nService.t.mockImplementation((id, p1) => { - if (id === "freeTrialEndPromptTomorrowNoOrgName") { - return oneDayRemainingTranslation; - } - - if (id === "freeTrialEndPromptCount" && p1 === 2) { - return twoDaysRemainingTranslation; - } - - return ""; - }); - - const organization1Subscription1 = await firstValueFrom( - organizationWarningsService.getFreeTrialWarning$(organization1), - ); - - const organization1Subscription2 = await firstValueFrom( - organizationWarningsService.getFreeTrialWarning$(organization1), - ); - - expect(organization1Subscription1).toEqual({ - organization: organization1, - message: oneDayRemainingTranslation, - }); - - expect(organization1Subscription2).toEqual(organization1Subscription1); - - const organization2Subscription1 = await firstValueFrom( - organizationWarningsService.getFreeTrialWarning$(organization2), - ); - - const organization2Subscription2 = await firstValueFrom( - organizationWarningsService.getFreeTrialWarning$(organization2), - ); - - expect(organization2Subscription1).toEqual({ - organization: organization2, - message: twoDaysRemainingTranslation, - }); - - expect(organization2Subscription2).toEqual(organization2Subscription1); - - expect(organizationBillingApiService.getWarnings).toHaveBeenCalledTimes(2); - }); - }); - - describe("getFreeTrialWarning$", () => { - it("should not emit a free trial warning when none is included in the warnings response", (done) => { - const organization = { - id: "1", - name: "Test", - } as Organization; - - organizationBillingApiService.getWarnings.mockReturnValue(empty()); - - const warning$ = organizationWarningsService.getFreeTrialWarning$(organization); - - warning$.subscribe({ - next: () => { - fail("Observable should not emit a value."); - }, - complete: () => { - done(); - }, - }); - }); - - it("should emit a free trial warning when one is included in the warnings response", async () => { - const response = respond({ - freeTrial: { - remainingTrialDays: 1, - }, - }); - - const organization = { - id: "1", - name: "Test", - } as Organization; - - organizationBillingApiService.getWarnings.mockImplementation((id) => { - if (id === organization.id) { - return response; - } else { - return empty(); - } - }); - - const translation = "translation"; - i18nService.t.mockImplementation((id) => { - if (id === "freeTrialEndPromptTomorrowNoOrgName") { - return translation; - } else { - return ""; - } - }); - - const warning = await firstValueFrom( - organizationWarningsService.getFreeTrialWarning$(organization), - ); - - expect(warning).toEqual({ - organization, - message: translation, - }); - }); - }); - - describe("getResellerRenewalWarning$", () => { - it("should not emit a reseller renewal warning when none is included in the warnings response", (done) => { - const organization = { - id: "1", - name: "Test", - } as Organization; - - organizationBillingApiService.getWarnings.mockReturnValue(empty()); - - const warning$ = organizationWarningsService.getResellerRenewalWarning$(organization); - - warning$.subscribe({ - next: () => { - fail("Observable should not emit a value."); - }, - complete: () => { - done(); - }, - }); - }); - - it("should emit a reseller renewal warning when one is included in the warnings response", async () => { - const response = respond({ - resellerRenewal: { - type: "upcoming", - upcoming: { - renewalDate: "2026-01-01T00:00:00.000Z", - }, - }, - }); - - const organization = { - id: "1", - name: "Test", - providerName: "Provider", - } as Organization; - - organizationBillingApiService.getWarnings.mockImplementation((id) => { - if (id === organization.id) { - return response; - } else { - return empty(); - } - }); - - const formattedDate = new Date("2026-01-01T00:00:00.000Z").toLocaleDateString("en-US", { - month: "short", - day: "2-digit", - year: "numeric", - }); - - const translation = "translation"; - i18nService.t.mockImplementation((id, p1, p2) => { - if ( - id === "resellerRenewalWarningMsg" && - p1 === organization.providerName && - p2 === formattedDate - ) { - return translation; - } else { - return ""; - } - }); - - const warning = await firstValueFrom( - organizationWarningsService.getResellerRenewalWarning$(organization), - ); - - expect(warning).toEqual({ - type: "info", - message: translation, - }); - }); - }); - - describe("showInactiveSubscriptionDialog$", () => { - it("should not emit the opening of a dialog for an inactive subscription warning when the warning is not included in the warnings response", (done) => { - const organization = { - id: "1", - name: "Test", - } as Organization; - - organizationBillingApiService.getWarnings.mockReturnValue(empty()); - - const warning$ = organizationWarningsService.showInactiveSubscriptionDialog$(organization); - - warning$.subscribe({ - next: () => { - fail("Observable should not emit a value."); - }, - complete: () => { - done(); - }, - }); - }); - - it("should emit the opening of a dialog for an inactive subscription warning when the warning is included in the warnings response", async () => { - const response = respond({ - inactiveSubscription: { - resolution: "add_payment_method", - }, - }); - - const organization = { - id: "1", - name: "Test", - providerName: "Provider", - } as Organization; - - organizationBillingApiService.getWarnings.mockImplementation((id) => { - if (id === organization.id) { - return response; - } else { - return empty(); - } - }); - - const titleTranslation = "title"; - const continueTranslation = "continue"; - const closeTranslation = "close"; - - i18nService.t.mockImplementation((id, param) => { - if (id === "suspendedOrganizationTitle" && param === organization.name) { - return titleTranslation; - } - if (id === "continue") { - return continueTranslation; - } - if (id === "close") { - return closeTranslation; - } - return ""; - }); - - const expectedOptions = { - title: titleTranslation, - content: { - key: "suspendedOwnerOrgMessage", - }, - type: "danger", - acceptButtonText: continueTranslation, - cancelButtonText: closeTranslation, - } as SimpleDialogOptions; - - dialogService.openSimpleDialog.mockImplementation((options) => { - if (JSON.stringify(options) == JSON.stringify(expectedOptions)) { - return Promise.resolve(true); - } else { - return Promise.resolve(false); - } - }); - - const observable$ = organizationWarningsService.showInactiveSubscriptionDialog$(organization); - - const routerNavigateSpy = jest.spyOn(router, "navigate").mockResolvedValue(true); - - await lastValueFrom(observable$); - - expect(routerNavigateSpy).toHaveBeenCalledWith( - ["organizations", `${organization.id}`, "billing", "payment-method"], - { - state: { launchPaymentModalAutomatically: true }, - }, - ); - }); - }); -}); diff --git a/apps/web/src/app/billing/warnings/types/index.ts b/apps/web/src/app/billing/warnings/types/index.ts index fc0c7d278ed..1d7b17fcd28 100644 --- a/apps/web/src/app/billing/warnings/types/index.ts +++ b/apps/web/src/app/billing/warnings/types/index.ts @@ -1 +1 @@ -export * from "./organization-warnings"; +export * from "./tax-id-warning-type"; diff --git a/apps/web/src/app/billing/warnings/types/organization-warnings.ts b/apps/web/src/app/billing/warnings/types/organization-warnings.ts deleted file mode 100644 index 96bf5aff6f1..00000000000 --- a/apps/web/src/app/billing/warnings/types/organization-warnings.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; - -export type OrganizationFreeTrialWarning = { - organization: Pick; - message: string; -}; - -export type OrganizationResellerRenewalWarning = { - type: "info" | "warning"; - message: string; -}; diff --git a/apps/web/src/app/billing/warnings/types/tax-id-warning-type.ts b/apps/web/src/app/billing/warnings/types/tax-id-warning-type.ts new file mode 100644 index 00000000000..86bc76708aa --- /dev/null +++ b/apps/web/src/app/billing/warnings/types/tax-id-warning-type.ts @@ -0,0 +1,19 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export const TaxIdWarningTypes = { + Missing: "tax_id_missing", + PendingVerification: "tax_id_pending_verification", + FailedVerification: "tax_id_failed_verification", +} as const; + +export type TaxIdWarningType = (typeof TaxIdWarningTypes)[keyof typeof TaxIdWarningTypes]; + +export class TaxIdWarningResponse extends BaseResponse { + type: TaxIdWarningType; + + constructor(response: any) { + super(response); + + this.type = this.getResponseProperty("Type"); + } +} diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 36d591cc390..ab4d21dab54 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -467,21 +467,60 @@ export class EventService { break; // Secrets Manager case EventType.Secret_Retrieved: - msg = this.i18nService.t("accessedSecretWithId", this.formatSecretId(ev)); + msg = this.i18nService.t("accessedSecretWithId", this.formatSecretId(ev, options)); humanReadableMsg = this.i18nService.t("accessedSecretWithId", this.getShortId(ev.secretId)); break; case EventType.Secret_Created: - msg = this.i18nService.t("createdSecretWithId", this.formatSecretId(ev)); + msg = this.i18nService.t("createdSecretWithId", this.formatSecretId(ev, options)); humanReadableMsg = this.i18nService.t("createdSecretWithId", this.getShortId(ev.secretId)); break; case EventType.Secret_Deleted: - msg = this.i18nService.t("deletedSecretWithId", this.formatSecretId(ev)); + msg = this.i18nService.t("deletedSecretWithId", this.formatSecretId(ev, options)); humanReadableMsg = this.i18nService.t("deletedSecretWithId", this.getShortId(ev.secretId)); break; + case EventType.Secret_Permanently_Deleted: + msg = this.i18nService.t( + "permanentlyDeletedSecretWithId", + this.formatSecretId(ev, options), + ); + humanReadableMsg = this.i18nService.t( + "permanentlyDeletedSecretWithId", + this.getShortId(ev.secretId), + ); + break; + case EventType.Secret_Restored: + msg = this.i18nService.t("restoredSecretWithId", this.formatSecretId(ev, options)); + humanReadableMsg = this.i18nService.t("restoredSecretWithId", this.getShortId(ev.secretId)); + break; case EventType.Secret_Edited: - msg = this.i18nService.t("editedSecretWithId", this.formatSecretId(ev)); + msg = this.i18nService.t("editedSecretWithId", this.formatSecretId(ev, options)); humanReadableMsg = this.i18nService.t("editedSecretWithId", this.getShortId(ev.secretId)); break; + case EventType.Project_Retrieved: + msg = this.i18nService.t("accessedProjectWithId", this.formatProjectId(ev, options)); + humanReadableMsg = this.i18nService.t( + "accessedProjectWithId", + this.getShortId(ev.projectId), + ); + break; + case EventType.Project_Created: + msg = this.i18nService.t("createdProjectWithId", this.formatProjectId(ev, options)); + humanReadableMsg = this.i18nService.t( + "createdProjectWithId", + this.getShortId(ev.projectId), + ); + break; + case EventType.Project_Deleted: + msg = this.i18nService.t("deletedProjectWithId", this.formatProjectId(ev, options)); + humanReadableMsg = this.i18nService.t( + "deletedProjectWithId", + this.getShortId(ev.projectId), + ); + break; + case EventType.Project_Edited: + msg = this.i18nService.t("editedProjectWithId", this.formatProjectId(ev, options)); + humanReadableMsg = this.i18nService.t("editedProjectWithId", this.getShortId(ev.projectId)); + break; default: break; } @@ -637,10 +676,41 @@ export class EventService { return a.outerHTML; } - formatSecretId(ev: EventResponse): string { + formatSecretId(ev: EventResponse, options: EventOptions): string { const shortId = this.getShortId(ev.secretId); + if (options.disableLink) { + return shortId; + } const a = this.makeAnchor(shortId); - a.setAttribute("href", "#/sm/" + ev.organizationId + "/secrets?search=" + shortId); + a.setAttribute( + "href", + "#/sm/" + + ev.organizationId + + "/secrets?search=" + + shortId + + "&viewEvents=" + + ev.secretId + + "&type=all", + ); + return a.outerHTML; + } + + formatProjectId(ev: EventResponse, options: EventOptions): string { + const shortId = this.getShortId(ev.projectId); + if (options.disableLink) { + return shortId; + } + const a = this.makeAnchor(shortId); + a.setAttribute( + "href", + "#/sm/" + + ev.organizationId + + "/projects?search=" + + shortId + + "&viewEvents=" + + ev.projectId + + "&type=all", + ); return a.outerHTML; } @@ -684,4 +754,5 @@ export class EventInfo { export class EventOptions { cipherInfo = true; + disableLink = false; } diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index f75d1268053..a3358ff7253 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -11,10 +11,10 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; 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 { IpcService } from "@bitwarden/common/platform/ipc"; import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { TaskService } from "@bitwarden/common/vault/tasks"; @@ -31,7 +31,6 @@ export class InitService { private i18nService: I18nServiceAbstraction, private eventUploadService: EventUploadServiceAbstraction, private twoFactorService: TwoFactorServiceAbstraction, - private stateService: StateServiceAbstraction, private keyService: KeyServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, @@ -41,13 +40,14 @@ export class InitService { private ipcService: IpcService, private sdkLoadService: SdkLoadService, private taskService: TaskService, + private readonly migrationRunner: MigrationRunner, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { await this.sdkLoadService.loadAndInit(); - await this.stateService.init(); + await this.migrationRunner.run(); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); if (activeAccount) { diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.html b/apps/web/src/app/dirt/reports/reports-layout.component.html index 283d9213cc7..a27556a7aa9 100644 --- a/apps/web/src/app/dirt/reports/reports-layout.component.html +++ b/apps/web/src/app/dirt/reports/reports-layout.component.html @@ -2,7 +2,7 @@
diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 4b0da4ae569..91c31853648 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -2,7 +2,7 @@ *ngIf="routeData$ | async as routeData" class="-tw-m-6 tw-mb-3 tw-flex tw-flex-col tw-p-6" [ngClass]="{ - 'tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-pb-0': + 'tw-border-0 tw-border-b tw-border-solid tw-border-secondary-100 tw-bg-background-alt tw-pb-0': tabsContainer.childElementCount !== 0, }" > @@ -30,7 +30,7 @@ diff --git a/apps/web/src/app/layouts/header/web-header.stories.ts b/apps/web/src/app/layouts/header/web-header.stories.ts index 9715dbf8cd3..7abddf01f2b 100644 --- a/apps/web/src/app/layouts/header/web-header.stories.ts +++ b/apps/web/src/app/layouts/header/web-header.stories.ts @@ -48,7 +48,7 @@ class MockStateService { @Component({ selector: "product-switcher", - template: ``, + template: ``, standalone: false, }) class MockProductSwitcher {} diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher.component.html index f1942a02c20..a44f05a7ed7 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.html @@ -3,7 +3,7 @@ bitIconButton="bwi bwi-fw bwi-filter" [bitMenuTriggerFor]="content?.menu" [buttonType]="buttonType" - [attr.aria-label]="'switchProducts' | i18n" + [label]="'switchProducts' | i18n" *ngIf="products$ | async" > 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 0b7304a3657..cc919a929a9 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 @@ -179,7 +179,7 @@ type Story = StoryObj< const Template: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
@@ -191,7 +191,7 @@ const Template: Story = {
- +
diff --git a/apps/web/src/app/settings/domain-rules.component.html b/apps/web/src/app/settings/domain-rules.component.html index 8ebeecb429f..880e2a6da0f 100644 --- a/apps/web/src/app/settings/domain-rules.component.html +++ b/apps/web/src/app/settings/domain-rules.component.html @@ -32,7 +32,7 @@ type="button" buttonType="danger" (click)="remove(i)" - appA11yTitle="{{ 'remove' | i18n }}" + label="{{ 'remove' | i18n }}" >
diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 637e1b77ce0..d7dbdbc4ae5 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -8,10 +8,7 @@ import { import { LayoutComponent, NavigationModule } from "@bitwarden/components"; import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component"; -import { EventsComponent as OrgEventsComponent } from "../admin-console/organizations/manage/events.component"; -import { UserConfirmComponent as OrgUserConfirmComponent } from "../admin-console/organizations/manage/user-confirm.component"; import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/manage/verify-recover-delete-org.component"; -import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component"; @@ -61,13 +58,10 @@ import { SharedModule } from "./shared.module"; PremiumBadgeComponent, ], declarations: [ - AcceptFamilySponsorshipComponent, - OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, OrgReusedPasswordsReportComponent, OrgUnsecuredWebsitesReportComponent, - OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, @@ -82,12 +76,10 @@ import { SharedModule } from "./shared.module"; UserVerificationModule, PremiumBadgeComponent, OrganizationLayoutComponent, - OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, OrgReusedPasswordsReportComponent, OrgUnsecuredWebsitesReportComponent, - OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, PremiumBadgeComponent, RecoverDeleteComponent, 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 64050eb9c06..b63cd15047b 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,13 +1,20 @@ import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core"; +import { UserId } from "@bitwarden/user-core"; @Injectable() export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction { constructor(private collectionAdminService: CollectionAdminService) {} - async getAllAdminCollections(organizationId: string): Promise { - return await this.collectionAdminService.getAll(organizationId); + async getAllAdminCollections( + organizationId: string, + userId: UserId, + ): Promise { + return await firstValueFrom( + this.collectionAdminService.collectionAdminViews$(organizationId, userId), + ); } } diff --git a/apps/web/src/app/tools/send/send-access/authentication-flow.md b/apps/web/src/app/tools/send/send-access/authentication-flow.md new file mode 100644 index 00000000000..f39b43fcd41 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/authentication-flow.md @@ -0,0 +1,75 @@ +# Send Authentication Flows + +In the below diagrams, activations represent client control flow. + +## Public Sends + +Anyone can access a public send. The token endpoint automatically issues a token. It never issues a challenge. + +```mermaid +sequenceDiagram + participant Visitor + participant TryAccess as try-send-access.guard + participant SendToken as send-token API + participant ViewContent as view-content.component + participant SendAccess as send-access API + + Visitor->>TryAccess: Navigate to send URL + activate TryAccess + TryAccess->>SendToken: Request anonymous access token + SendToken-->>TryAccess: OK + Security token + TryAccess->>ViewContent: Redirect with token + deactivate TryAccess + activate ViewContent + ViewContent->>SendAccess: Request send content (with token and key) + SendAccess-->>ViewContent: Return send content + ViewContent->>Visitor: Display send content + deactivate ViewContent +``` + +## Password Protected Sends + +Password protected sends redirect to a password challenge prompt. + +```mermaid +sequenceDiagram + participant Visitor + participant TryAccess as try-send-access.guard + participant PasswordAuth as password-authentication.component + participant SendToken as send-token API + participant ViewContent as view-content.component + participant SendAccess as send-access API + + Visitor->>TryAccess: Navigate to send URL + activate TryAccess + TryAccess->>SendToken: Request anonymous access token + SendToken-->>TryAccess: Unauthorized + Password challenge + TryAccess->>PasswordAuth: Redirect with send ID and key + deactivate TryAccess + activate PasswordAuth + PasswordAuth->>Visitor: Request password + Visitor-->>PasswordAuth: Enter password + PasswordAuth->>SendToken: Request access token (with password) + SendToken-->>PasswordAuth: OK + Security token + deactivate PasswordAuth + activate ViewContent + PasswordAuth->>ViewContent: Redirect with token and send key + ViewContent->>SendAccess: Request send content (with token) + SendAccess-->>ViewContent: Return send content + ViewContent->>Visitor: Display send content + deactivate ViewContent +``` + +## Send Access without token + +Visiting the view page without a token redirects to a try-access flow, above. + +```mermaid +sequenceDiagram + participant Visitor + participant ViewContent as view-content.component + participant TryAccess as try-send-access.guard + + Visitor->>ViewContent: Navigate to send URL (with id and key) + ViewContent->>TryAccess: Redirect to try-access (with id and key) +``` diff --git a/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts b/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts new file mode 100644 index 00000000000..cd07d3684fb --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts @@ -0,0 +1,175 @@ +import { TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { Router, UrlTree } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, NEVER } from "rxjs"; + +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { mockAccountServiceWith, FakeStateProvider } from "@bitwarden/common/spec"; +import { SemanticLogger } from "@bitwarden/common/tools/log"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { UserId } from "@bitwarden/common/types/guid"; +import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; + +import { DefaultSendAccessService } from "./default-send-access-service"; +import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory"; + +describe("DefaultSendAccessService", () => { + let service: DefaultSendAccessService; + let stateProvider: FakeStateProvider; + let sendApiService: MockProxy; + let router: MockProxy; + let logger: MockProxy; + let systemServiceProvider: MockProxy; + + beforeEach(() => { + const accountService = mockAccountServiceWith("user-id" as UserId); + stateProvider = new FakeStateProvider(accountService); + sendApiService = mock(); + router = mock(); + logger = mock(); + systemServiceProvider = mock(); + + systemServiceProvider.log.mockReturnValue(logger); + + TestBed.configureTestingModule({ + providers: [ + DefaultSendAccessService, + { provide: StateProvider, useValue: stateProvider }, + { provide: SendApiService, useValue: sendApiService }, + { provide: Router, useValue: router }, + { provide: SYSTEM_SERVICE_PROVIDER, useValue: systemServiceProvider }, + ], + }); + + service = TestBed.inject(DefaultSendAccessService); + }); + + describe("constructor", () => { + it("creates logger with type 'SendAccessAuthenticationService' when initialized", () => { + expect(systemServiceProvider.log).toHaveBeenCalledWith({ + type: "SendAccessAuthenticationService", + }); + }); + }); + + describe("redirect$", () => { + const sendId = "test-send-id"; + + it("returns content page UrlTree and logs info when API returns success", async () => { + const expectedUrlTree = { toString: () => "/send/content/test-send-id" } as UrlTree; + sendApiService.postSendAccess.mockResolvedValue({} as any); + router.createUrlTree.mockReturnValue(expectedUrlTree); + + const result = await firstValueFrom(service.redirect$(sendId)); + + expect(result).toBe(expectedUrlTree); + expect(logger.info).toHaveBeenCalledWith( + "public send detected; redirecting to send access with token.", + ); + }); + + describe("given error responses", () => { + it("returns password flow UrlTree and logs debug when 401 received", async () => { + const expectedUrlTree = { toString: () => "/send/test-send-id" } as UrlTree; + const errorResponse = new ErrorResponse([], 401); + sendApiService.postSendAccess.mockRejectedValue(errorResponse); + router.createUrlTree.mockReturnValue(expectedUrlTree); + + const result = await firstValueFrom(service.redirect$(sendId)); + + expect(result).toBe(expectedUrlTree); + expect(logger.debug).toHaveBeenCalledWith(errorResponse, "redirecting to password flow"); + }); + + it("returns 404 page UrlTree and logs debug when 404 received", async () => { + const expectedUrlTree = { toString: () => "/404.html" } as UrlTree; + const errorResponse = new ErrorResponse([], 404); + sendApiService.postSendAccess.mockRejectedValue(errorResponse); + router.parseUrl.mockReturnValue(expectedUrlTree); + + const result = await firstValueFrom(service.redirect$(sendId)); + + expect(result).toBe(expectedUrlTree); + expect(logger.debug).toHaveBeenCalledWith(errorResponse, "redirecting to unavailable page"); + }); + + it("logs warning and throws error when 500 received", async () => { + const errorResponse = new ErrorResponse([], 500); + sendApiService.postSendAccess.mockRejectedValue(errorResponse); + + await expect(firstValueFrom(service.redirect$(sendId))).rejects.toBe(errorResponse); + expect(logger.warn).toHaveBeenCalledWith( + errorResponse, + "received unexpected error response", + ); + }); + + it("throws error when unexpected error code received", async () => { + const errorResponse = new ErrorResponse([], 403); + sendApiService.postSendAccess.mockRejectedValue(errorResponse); + + await expect(firstValueFrom(service.redirect$(sendId))).rejects.toBe(errorResponse); + expect(logger.warn).toHaveBeenCalledWith( + errorResponse, + "received unexpected error response", + ); + }); + }); + + it("throws error when non-ErrorResponse error occurs", async () => { + const regularError = new Error("Network error"); + sendApiService.postSendAccess.mockRejectedValue(regularError); + + await expect(firstValueFrom(service.redirect$(sendId))).rejects.toThrow("Network error"); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it("emits timeout error when API response exceeds 10 seconds", fakeAsync(() => { + // Mock API to never resolve (simulating a hung request) + sendApiService.postSendAccess.mockReturnValue(firstValueFrom(NEVER)); + + const result$ = service.redirect$(sendId); + let error: any; + + result$.subscribe({ + error: (err: unknown) => (error = err), + }); + + // Advance time past 10 seconds + tick(10001); + + expect(error).toBeDefined(); + expect(error.name).toBe("TimeoutError"); + })); + }); + + describe("setContext", () => { + it("updates global state with send context when called with sendId and key", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + + await service.setContext(sendId, key); + + const context = await firstValueFrom(stateProvider.getGlobal(SEND_CONTEXT_KEY).state$); + expect(context).toEqual({ id: sendId, key }); + }); + }); + + describe("clear", () => { + it("sets both SEND_RESPONSE_KEY and SEND_CONTEXT_KEY to null when called", async () => { + // Set initial values + await stateProvider.getGlobal(SEND_RESPONSE_KEY).update(() => ({ some: "response" }) as any); + await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: "test", key: "test" })); + + await service.clear(); + + const response = await firstValueFrom(stateProvider.getGlobal(SEND_RESPONSE_KEY).state$); + const context = await firstValueFrom(stateProvider.getGlobal(SEND_CONTEXT_KEY).state$); + + expect(response).toBeNull(); + expect(context).toBeNull(); + }); + }); +}); diff --git a/apps/web/src/app/tools/send/send-access/default-send-access-service.ts b/apps/web/src/app/tools/send/send-access/default-send-access-service.ts new file mode 100644 index 00000000000..732303ce25a --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/default-send-access-service.ts @@ -0,0 +1,96 @@ +import { Injectable, Inject } from "@angular/core"; +import { Router, UrlTree } from "@angular/router"; +import { map, of, from, catchError, timeout } from "rxjs"; + +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { SemanticLogger } from "@bitwarden/common/tools/log"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; + +import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory"; +import { SendAccessService } from "./send-access-service.abstraction"; +import { isErrorResponse } from "./util"; + +const TEN_SECONDS = 10_000; + +@Injectable({ providedIn: "root" }) +export class DefaultSendAccessService implements SendAccessService { + private readonly logger: SemanticLogger; + + constructor( + private readonly state: StateProvider, + private readonly api: SendApiService, + private readonly router: Router, + @Inject(SYSTEM_SERVICE_PROVIDER) system: SystemServiceProvider, + ) { + this.logger = system.log({ type: "SendAccessAuthenticationService" }); + } + + redirect$(sendId: string) { + // FIXME: when the send authentication APIs become available, this method + // should delegate to the API + const response$ = from(this.api.postSendAccess(sendId, new SendAccessRequest())); + + const redirect$ = response$.pipe( + timeout({ first: TEN_SECONDS }), + map((_response) => { + this.logger.info("public send detected; redirecting to send access with token."); + const url = this.toViewRedirect(sendId); + + return url; + }), + catchError((error: unknown) => { + let processed: UrlTree | undefined = undefined; + + if (isErrorResponse(error)) { + processed = this.toErrorRedirect(sendId, error); + } + + if (processed) { + return of(processed); + } + + throw error; + }), + ); + + return redirect$; + } + + private toViewRedirect(sendId: string) { + return this.router.createUrlTree(["send", "content", sendId]); + } + + private toErrorRedirect(sendId: string, response: ErrorResponse) { + let url: UrlTree | undefined = undefined; + + switch (response.statusCode) { + case 401: + this.logger.debug(response, "redirecting to password flow"); + url = this.router.createUrlTree(["send/password", sendId]); + break; + + case 404: + this.logger.debug(response, "redirecting to unavailable page"); + url = this.router.parseUrl("/404.html"); + break; + + default: + this.logger.warn(response, "received unexpected error response"); + } + + return url; + } + + async setContext(sendId: string, key: string) { + await this.state.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key })); + } + + async clear(): Promise { + await this.state.getGlobal(SEND_RESPONSE_KEY).update(() => null); + await this.state.getGlobal(SEND_CONTEXT_KEY).update(() => null); + } +} diff --git a/apps/web/src/app/tools/send/send-access/index.ts b/apps/web/src/app/tools/send/send-access/index.ts index c9df5ce5193..4bef65f468b 100644 --- a/apps/web/src/app/tools/send/send-access/index.ts +++ b/apps/web/src/app/tools/send/send-access/index.ts @@ -1,2 +1,4 @@ export { AccessComponent } from "./access.component"; export { SendAccessExplainerComponent } from "./send-access-explainer.component"; + +export { SendAccessRoutes } from "./routes"; diff --git a/apps/web/src/app/tools/send/send-access/routes.ts b/apps/web/src/app/tools/send/send-access/routes.ts new file mode 100644 index 00000000000..4f794aecd23 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/routes.ts @@ -0,0 +1,60 @@ +import { Routes } from "@angular/router"; + +import { AnonLayoutWrapperData } from "@bitwarden/components"; +import { ActiveSendIcon } from "@bitwarden/send-ui"; + +import { RouteDataProperties } from "../../../core"; + +import { SendAccessExplainerComponent } from "./send-access-explainer.component"; +import { SendAccessPasswordComponent } from "./send-access-password.component"; +import { trySendAccess } from "./try-send-access.guard"; + +/** Routes to reach send access screens */ +export const SendAccessRoutes: Routes = [ + { + path: "send/:sendId", + // there are no child pages because `trySendAccess` always performs a redirect + canActivate: [trySendAccess], + }, + { + path: "send/password/:sendId", + data: { + pageTitle: { + key: "sendAccessPasswordTitle", + }, + pageIcon: ActiveSendIcon, + showReadonlyHostname: true, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { + path: "", + component: SendAccessPasswordComponent, + }, + { + path: "", + outlet: "secondary", + component: SendAccessExplainerComponent, + }, + ], + }, + { + path: "send/content/:sendId", + data: { + pageTitle: { + key: "sendAccessContentTitle", + }, + pageIcon: ActiveSendIcon, + showReadonlyHostname: true, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { + path: "send/password/:sendId", + }, + { + path: "", + outlet: "secondary", + component: SendAccessExplainerComponent, + }, + ], + }, +]; diff --git a/apps/web/src/app/tools/send/send-access/send-access-memory.spec.ts b/apps/web/src/app/tools/send/send-access/send-access-memory.spec.ts new file mode 100644 index 00000000000..8d7fe9cd380 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-access-memory.spec.ts @@ -0,0 +1,50 @@ +import { KeyDefinition, SEND_ACCESS_AUTH_MEMORY } from "@bitwarden/common/platform/state"; +import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; + +import { SEND_CONTEXT_KEY, SEND_RESPONSE_KEY } from "./send-access-memory"; +import { SendContext } from "./types"; + +describe("send-access-memory", () => { + describe("SEND_CONTEXT_KEY", () => { + it("has correct state definition properties", () => { + expect(SEND_CONTEXT_KEY).toBeInstanceOf(KeyDefinition); + expect(SEND_CONTEXT_KEY.stateDefinition).toBe(SEND_ACCESS_AUTH_MEMORY); + expect(SEND_CONTEXT_KEY.key).toBe("sendContext"); + }); + + it("deserializes data as-is", () => { + const testContext: SendContext = { id: "test-id", key: "test-key" }; + const deserializer = SEND_CONTEXT_KEY.deserializer; + expect(deserializer(testContext)).toBe(testContext); + }); + + it("deserializes null as null", () => { + const deserializer = SEND_CONTEXT_KEY.deserializer; + expect(deserializer(null)).toBe(null); + }); + }); + + describe("SEND_RESPONSE_KEY", () => { + it("has correct state definition properties", () => { + expect(SEND_RESPONSE_KEY).toBeInstanceOf(KeyDefinition); + expect(SEND_RESPONSE_KEY.stateDefinition).toBe(SEND_ACCESS_AUTH_MEMORY); + expect(SEND_RESPONSE_KEY.key).toBe("sendResponse"); + }); + + it("deserializes data into SendAccessResponse instance", () => { + const mockData = { id: "test-id", name: "test-send" } as any; + const deserializer = SEND_RESPONSE_KEY.deserializer; + const result = deserializer(mockData); + + expect(result).toBeInstanceOf(SendAccessResponse); + }); + + it.each([ + [null, "null"], + [undefined, "undefined"], + ])("deserializes %s as null", (value, _) => { + const deserializer = SEND_RESPONSE_KEY.deserializer; + expect(deserializer(value!)).toBe(null); + }); + }); +}); diff --git a/apps/web/src/app/tools/send/send-access/send-access-memory.ts b/apps/web/src/app/tools/send/send-access/send-access-memory.ts new file mode 100644 index 00000000000..4f67cf43b37 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-access-memory.ts @@ -0,0 +1,25 @@ +import { KeyDefinition, SEND_ACCESS_AUTH_MEMORY } from "@bitwarden/common/platform/state"; +import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; + +import { SendContext } from "./types"; + +export const SEND_CONTEXT_KEY = new KeyDefinition( + SEND_ACCESS_AUTH_MEMORY, + "sendContext", + { + deserializer: (data) => data, + }, +); + +/** When send authentication succeeds, this stores the result so that + * multiple access attempts don't accrue due to the send workflow. + */ +// FIXME: replace this with the send authentication token once it's +// available +export const SEND_RESPONSE_KEY = new KeyDefinition( + SEND_ACCESS_AUTH_MEMORY, + "sendResponse", + { + deserializer: (data) => (data ? new SendAccessResponse(data) : null), + }, +); diff --git a/apps/web/src/app/tools/send/send-access/send-access-service.abstraction.ts b/apps/web/src/app/tools/send/send-access/send-access-service.abstraction.ts new file mode 100644 index 00000000000..66fc87fe802 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-access-service.abstraction.ts @@ -0,0 +1,10 @@ +import { UrlTree } from "@angular/router"; +import { Observable } from "rxjs"; + +export abstract class SendAccessService { + abstract redirect$: (sendId: string) => Observable; + + abstract setContext: (sendId: string, key: string) => Promise; + + abstract clear: () => Promise; +} diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts new file mode 100644 index 00000000000..267de83db9f --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts @@ -0,0 +1,426 @@ +import { TestBed } from "@angular/core/testing"; +import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from "@angular/router"; +import { firstValueFrom, Observable, of } from "rxjs"; + +import { SemanticLogger } from "@bitwarden/common/tools/log"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; + +import { SendAccessService } from "./send-access-service.abstraction"; +import { trySendAccess } from "./try-send-access.guard"; + +function createMockRoute(params: Record): ActivatedRouteSnapshot { + return { params } as ActivatedRouteSnapshot; +} + +function createMockLogger(): SemanticLogger { + return { + warn: jest.fn(), + panic: jest.fn().mockImplementation(() => { + throw new Error("Logger panic called"); + }), + } as any as SemanticLogger; +} + +function createMockSystemServiceProvider(): SystemServiceProvider { + return { + log: jest.fn().mockReturnValue(createMockLogger()), + } as any as SystemServiceProvider; +} + +function createMockSendAccessService() { + return { + setContext: jest.fn().mockResolvedValue(undefined), + redirect$: jest.fn().mockReturnValue(of({} as UrlTree)), + clear: jest.fn().mockResolvedValue(undefined), + }; +} + +describe("trySendAccess", () => { + let mockSendAccessService: ReturnType; + let mockSystemServiceProvider: SystemServiceProvider; + let mockRouterState: RouterStateSnapshot; + + beforeEach(() => { + mockSendAccessService = createMockSendAccessService(); + mockSystemServiceProvider = createMockSystemServiceProvider(); + mockRouterState = {} as RouterStateSnapshot; + + TestBed.configureTestingModule({ + providers: [ + { provide: SendAccessService, useValue: mockSendAccessService }, + { provide: SYSTEM_SERVICE_PROVIDER, useValue: mockSystemServiceProvider }, + ], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("canActivate", () => { + describe("given valid route parameters", () => { + it("extracts sendId and key from route params when both are valid strings", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + const mockRoute = createMockRoute({ sendId, key }); + const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; + mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); + + // need to cast the result because `CanActivateFn` performs type erasure + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + + expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key); + expect(mockSendAccessService.setContext).toHaveBeenCalledTimes(1); + await expect(firstValueFrom(result$)).resolves.toEqual(expectedUrlTree); + }); + + it("does not throw validation errors when sendId and key are valid strings", async () => { + const sendId = "valid-send-id"; + const key = "valid-key"; + const mockRoute = createMockRoute({ sendId, key }); + const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; + mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); + + // Should not throw any errors during guard execution + let guardResult: Observable | undefined; + expect(() => { + guardResult = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + }).not.toThrow(); + + // Verify the observable can be subscribed to without errors + expect(guardResult).toBeDefined(); + await expect(firstValueFrom(guardResult!)).resolves.toEqual(expectedUrlTree); + + // Logger methods should not be called for warnings or panics + const mockLogger = (mockSystemServiceProvider.log as jest.Mock).mock.results[0].value; + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.panic).not.toHaveBeenCalled(); + }); + }); + + describe("given invalid route parameters", () => { + describe("given invalid sendId", () => { + it.each([ + ["undefined", undefined], + ["null", null], + ])( + "logs warning with correct message when sendId is %s", + async (description, sendIdValue) => { + const key = "valid-key"; + const mockRoute = createMockRoute( + sendIdValue === undefined ? { key } : { sendId: sendIdValue, key }, + ); + const mockLogger = createMockLogger(); + (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + + await expect(async () => { + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + await firstValueFrom(result$); + }).rejects.toThrow("Logger panic called"); + + expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ + function: "trySendAccess", + }); + expect(mockLogger.warn).toHaveBeenCalledWith( + "sendId missing from the route parameters; redirecting to 404", + ); + }, + ); + + it.each([ + ["number", 123], + ["object", {}], + ["boolean", true], + ])("logs panic with expected/actual type info when sendId is %s", async (type, value) => { + const key = "valid-key"; + const mockRoute = createMockRoute({ sendId: value, key }); + const mockLogger = createMockLogger(); + (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + + await expect(async () => { + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + await firstValueFrom(result$); + }).rejects.toThrow("Logger panic called"); + + expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); + expect(mockLogger.panic).toHaveBeenCalledWith( + { expected: "string", actual: type }, + "sendId has invalid type", + ); + }); + + it("throws when sendId is not a string", async () => { + const key = "valid-key"; + const invalidSendIdValues = [123, {}, true, null, undefined]; + + for (const invalidSendId of invalidSendIdValues) { + const mockRoute = createMockRoute( + invalidSendId === undefined ? { key } : { sendId: invalidSendId, key }, + ); + const mockLogger = createMockLogger(); + (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + + await expect(async () => { + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + await firstValueFrom(result$); + }).rejects.toThrow("Logger panic called"); + } + }); + }); + + describe("given invalid key", () => { + it.each([ + ["undefined", undefined], + ["null", null], + ])("logs panic with correct message when key is %s", async (description, keyValue) => { + const sendId = "valid-send-id"; + const mockRoute = createMockRoute( + keyValue === undefined ? { sendId } : { sendId, key: keyValue }, + ); + const mockLogger = createMockLogger(); + (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + + await expect(async () => { + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + await firstValueFrom(result$); + }).rejects.toThrow("Logger panic called"); + + expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); + expect(mockLogger.panic).toHaveBeenCalledWith("key missing from the route parameters"); + }); + + it.each([ + ["number", 123], + ["object", {}], + ["boolean", true], + ])("logs panic with expected/actual type info when key is %s", async (type, value) => { + const sendId = "valid-send-id"; + const mockRoute = createMockRoute({ sendId, key: value }); + const mockLogger = createMockLogger(); + (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + + await expect(async () => { + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + await firstValueFrom(result$); + }).rejects.toThrow("Logger panic called"); + + expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); + expect(mockLogger.panic).toHaveBeenCalledWith( + { expected: "string", actual: type }, + "key has invalid type", + ); + }); + + it("throws when key is not a string", async () => { + const sendId = "valid-send-id"; + const invalidKeyValues = [123, {}, true, null, undefined]; + + for (const invalidKey of invalidKeyValues) { + const mockRoute = createMockRoute( + invalidKey === undefined ? { sendId } : { sendId, key: invalidKey }, + ); + const mockLogger = createMockLogger(); + (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + + await expect(async () => { + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + await firstValueFrom(result$); + }).rejects.toThrow("Logger panic called"); + } + }); + }); + }); + + describe("given service interactions", () => { + it("calls setContext with extracted sendId and key when parameters are valid", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + const mockRoute = createMockRoute({ sendId, key }); + const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; + mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); + + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + + await firstValueFrom(result$); + + expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key); + expect(mockSendAccessService.setContext).toHaveBeenCalledTimes(1); + }); + + it("calls redirect$ with extracted sendId when setContext completes", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + const mockRoute = createMockRoute({ sendId, key }); + const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; + mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); + + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + + await firstValueFrom(result$); + + expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId); + expect(mockSendAccessService.redirect$).toHaveBeenCalledTimes(1); + }); + }); + + describe("given observable behavior", () => { + it("returns redirect$ emissions when setContext completes successfully", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + const mockRoute = createMockRoute({ sendId, key }); + const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; + mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); + + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + + const actualResult = await firstValueFrom(result$); + + expect(actualResult).toEqual(expectedUrlTree); + expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId); + }); + + it("does not emit setContext values when using ignoreElements", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + const mockRoute = createMockRoute({ sendId, key }); + const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; + const setContextValue = "should-not-be-emitted"; + + // Mock setContext to return a value + mockSendAccessService.setContext.mockResolvedValue(setContextValue); + mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); + + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + + const actualResult = await firstValueFrom(result$); + + // Should only emit the redirect$ value, not the setContext value + expect(actualResult).toEqual(expectedUrlTree); + expect(actualResult).not.toEqual(setContextValue); + }); + + it("ensures setContext completes before redirect$ executes (sequencing)", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + const mockRoute = createMockRoute({ sendId, key }); + const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; + + let setContextResolved = false; + + // Mock setContext to track when it resolves + mockSendAccessService.setContext.mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); // Small delay + setContextResolved = true; + }); + + // Mock redirect$ to return a delayed observable and check if setContext resolved + mockSendAccessService.redirect$.mockImplementation((id) => { + return new Observable((subscriber) => { + // Check if setContext has resolved when redirect$ subscription starts + setTimeout(() => { + expect(setContextResolved).toBe(true); + subscriber.next(expectedUrlTree); + subscriber.complete(); + }, 0); + }); + }); + + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + + await firstValueFrom(result$); + }); + }); + + describe("given error scenarios", () => { + it("does not call redirect$ when setContext rejects", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + const mockRoute = createMockRoute({ sendId, key }); + const setContextError = new Error("setContext failed"); + + // Reset mocks to ensure clean state + jest.clearAllMocks(); + + // Mock setContext to reject + mockSendAccessService.setContext.mockRejectedValue(setContextError); + + // Create a mock observable that we can spy on subscription + const mockRedirectObservable = of({} as UrlTree); + const subscribeSpy = jest.spyOn(mockRedirectObservable, "subscribe"); + mockSendAccessService.redirect$.mockReturnValue(mockRedirectObservable); + + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + + // Expect the observable to reject when setContext fails + await expect(firstValueFrom(result$)).rejects.toThrow("setContext failed"); + + // The redirect$ method will be called (since it's called synchronously) + expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId); + + // But the returned observable should not be subscribed to due to the error + // Note: This test verifies the error propagation behavior + expect(subscribeSpy).not.toHaveBeenCalled(); + }); + + it("propagates error to guard return value when redirect$ throws", async () => { + const sendId = "test-send-id"; + const key = "test-key"; + const mockRoute = createMockRoute({ sendId, key }); + const redirectError = new Error("redirect$ failed"); + + // Reset mocks to ensure clean state + jest.clearAllMocks(); + + // Mock setContext to succeed and redirect$ to throw + mockSendAccessService.setContext.mockResolvedValue(undefined); + mockSendAccessService.redirect$.mockReturnValue( + new Observable((subscriber) => { + subscriber.error(redirectError); + }), + ); + + const result$ = TestBed.runInInjectionContext(() => + trySendAccess(mockRoute, mockRouterState), + ) as unknown as Observable; + + // Expect the observable to propagate the redirect$ error + await expect(firstValueFrom(result$)).rejects.toThrow("redirect$ failed"); + + // Verify that setContext was called (should succeed) + expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key); + + // Verify that redirect$ was called (but it throws) + expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId); + }); + }); + }); +}); diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts new file mode 100644 index 00000000000..51941bf8e74 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts @@ -0,0 +1,38 @@ +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from "@angular/router"; +import { from, ignoreElements, concat } from "rxjs"; + +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; + +import { SendAccessService } from "./send-access-service.abstraction"; + +export const trySendAccess: CanActivateFn = ( + route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot, +) => { + const sendAccess = inject(SendAccessService); + const system = inject(SYSTEM_SERVICE_PROVIDER); + const logger = system.log({ function: "trySendAccess" }); + + const { sendId, key } = route.params; + if (!sendId) { + logger.warn("sendId missing from the route parameters; redirecting to 404"); + } + if (typeof sendId !== "string") { + logger.panic({ expected: "string", actual: typeof sendId }, "sendId has invalid type"); + } + + if (!key) { + logger.panic("key missing from the route parameters"); + } + if (typeof key !== "string") { + logger.panic({ expected: "string", actual: typeof key }, "key has invalid type"); + } + + const contextUpdated$ = from(sendAccess.setContext(sendId, key)).pipe(ignoreElements()); + const redirect$ = sendAccess.redirect$(sendId); + + // ensure the key has loaded before redirecting + return concat(contextUpdated$, redirect$); +}; diff --git a/apps/web/src/app/tools/send/send-access/types.ts b/apps/web/src/app/tools/send/send-access/types.ts new file mode 100644 index 00000000000..03e058ca681 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/types.ts @@ -0,0 +1,8 @@ +/** global contextual information for the current send access page. */ +export type SendContext = { + /** identifies the send */ + id: string; + + /** decrypts the send content */ + key: string; +}; diff --git a/apps/web/src/app/tools/send/send-access/util.spec.ts b/apps/web/src/app/tools/send/send-access/util.spec.ts new file mode 100644 index 00000000000..45502ee2509 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/util.spec.ts @@ -0,0 +1,69 @@ +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; + +import { isErrorResponse, isSendContext } from "./util"; + +describe("util", () => { + describe("isErrorResponse", () => { + it("returns true when value is an ErrorResponse instance", () => { + const error = new ErrorResponse(["Error message"], 400); + expect(isErrorResponse(error)).toBe(true); + }); + + it.each([ + [null, "null"], + [undefined, "undefined"], + ])("returns false when value is %s", (value, description) => { + expect(isErrorResponse(value)).toBe(false); + }); + + it.each([ + ["string", "string"], + [123, "number"], + [true, "boolean"], + [{}, "plain object"], + [[], "array"], + ])("returns false when value is not an ErrorResponse (%s)", (value, description) => { + expect(isErrorResponse(value)).toBe(false); + }); + + it("returns false when value is a different Error type", () => { + const error = new Error("test"); + expect(isErrorResponse(error)).toBe(false); + }); + }); + + describe("isSendContext", () => { + it("returns true when value has id and key properties", () => { + const validContext = { id: "test-id", key: "test-key" }; + expect(isSendContext(validContext)).toBe(true); + }); + + it("returns true even with additional properties", () => { + const contextWithExtras = { id: "test-id", key: "test-key", extra: "data" }; + expect(isSendContext(contextWithExtras)).toBe(true); + }); + + it.each([ + [null, "null"], + [undefined, "undefined"], + ])("returns false when value is %s", (value, _) => { + expect(isSendContext(value)).toBe(false); + }); + + it.each([ + ["string", "string"], + [123, "number"], + [true, "boolean"], + ])("returns false when value is not an object (%s)", (value, _) => { + expect(isSendContext(value)).toBe(false); + }); + + it.each([ + [{ key: "test-key" }, "missing id"], + [{ id: "test-id" }, "missing key"], + [{}, "empty object"], + ])("returns false when value is %s", (value, _) => { + expect(isSendContext(value)).toBe(false); + }); + }); +}); diff --git a/apps/web/src/app/tools/send/send-access/util.ts b/apps/web/src/app/tools/send/send-access/util.ts new file mode 100644 index 00000000000..d9cbef0d337 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/util.ts @@ -0,0 +1,13 @@ +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; + +import { SendContext } from "./types"; + +/** narrows a type to an `ErrorResponse` */ +export function isErrorResponse(value: unknown): value is ErrorResponse { + return value instanceof ErrorResponse; +} + +/** narrows a type to a `SendContext` */ +export function isSendContext(value: unknown): value is SendContext { + return !!value && typeof value === "object" && "id" in value && "key" in value; +} diff --git a/apps/web/src/app/tools/send/send.component.html b/apps/web/src/app/tools/send/send.component.html index 042046b85ff..b79f50311ed 100644 --- a/apps/web/src/app/tools/send/send.component.html +++ b/apps/web/src/app/tools/send/send.component.html @@ -156,7 +156,7 @@ type="button" [bitMenuTriggerFor]="sendOptions" bitIconButton="bwi-ellipsis-v" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" > @@ -101,7 +101,7 @@ bitIconButton="bwi-ellipsis-v" type="button" appStopProp - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" > diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index ad2886b1e59..7cd5129d3f0 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -65,7 +65,7 @@ size="small" bitIconButton="bwi-ellipsis-v" type="button" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" appStopProp > } 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 ef928903a72..5ddccf6a395 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 @@ -56,7 +56,7 @@ bitIconButton="bwi-ellipsis-v" size="small" type="button" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" >
- + @@ -67,8 +67,7 @@ bitIconButton="bwi-trash" buttonType="danger" size="default" - title="{{ 'delete' | i18n }}" - aria-label="Delete" + label="{{ 'delete' | i18n }}" [bitAction]="deleteDomain" type="submit" bitFormButton diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html index 7ade2e6c63d..38cb077c623 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html @@ -29,7 +29,7 @@ bitSuffix bitIconButton="bwi-clone" [bitAction]="copyScimUrl" - [appA11yTitle]="'copyScimUrl' | i18n" + [label]="'copyScimUrl' | i18n" > @@ -46,7 +46,7 @@ bitSuffix [bitIconButton]="showScimKey ? 'bwi-eye-slash' : 'bwi-eye'" [bitAction]="toggleScimKey" - [appA11yTitle]="'toggleVisibility' | i18n" + [label]="'toggleVisibility' | i18n" > {{ "scimApiKeyHelperText" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.html index 60993f5570c..08e694aa45a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.html @@ -58,7 +58,7 @@ bitIconButton="bwi-trash" buttonType="danger" bitFormButton - [appA11yTitle]="'delete' | i18n" + [label]="'delete' | i18n" [bitAction]="delete" [disabled]="loading" > diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts index 2ad2ecdccbd..d8ad2e01343 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts @@ -5,7 +5,9 @@ import { ActivatedRoute, Router } from "@angular/router"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -41,6 +43,8 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { private userNamePipe: UserNamePipe, fileDownloadService: FileDownloadService, toastService: ToastService, + accountService: AccountService, + organizationService: OrganizationService, ) { super( eventService, @@ -50,6 +54,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { logService, fileDownloadService, toastService, + route, + accountService, + organizationService, ); } @@ -69,6 +76,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { } async load() { + this.initBase(); const response = await this.apiService.getProviderUsers(this.providerId); response.data.forEach((u) => { const name = this.userNamePipe.transform(u); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html index f203b7a934a..07ccd997b96 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html @@ -79,7 +79,7 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" >

{{ "setupProviderDesc" | i18n }}

- @if (!(requireProviderPaymentMethodDuringSetup$ | async)) { -

{{ "generalInformation" | i18n }}

-
-
- - {{ "providerName" | i18n }} - - -
-
- - {{ "billingEmail" | i18n }} - - {{ "providerBillingEmailHint" | i18n }} - -
+

{{ "billingInformation" | i18n }}

+
+
+ + {{ "providerName" | i18n }} + +
- - } @else { -

{{ "billingInformation" | i18n }}

-
-
- - {{ "providerName" | i18n }} - - -
-
- - {{ "billingEmail" | i18n }} - - {{ "providerBillingEmailHint" | i18n }} - -
+
+ + {{ "billingEmail" | i18n }} + + {{ "providerBillingEmailHint" | i18n }} +
-

{{ "paymentMethod" | i18n }}

- - - } +
+

{{ "paymentMethod" | i18n }}

+ + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index 53b54e459ea..edad6616fb3 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -3,7 +3,7 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, Subject, switchMap } from "rxjs"; +import { Subject, switchMap } from "rxjs"; import { first, takeUntil } from "rxjs/operators"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; @@ -11,8 +11,6 @@ import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/a import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -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 { ProviderKey } from "@bitwarden/common/types/key"; @@ -41,10 +39,6 @@ export class SetupComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); - requireProviderPaymentMethodDuringSetup$ = this.configService.getFeatureFlag$( - FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup, - ); - constructor( private router: Router, private i18nService: I18nService, @@ -52,7 +46,6 @@ export class SetupComponent implements OnInit, OnDestroy { private keyService: KeyService, private syncService: SyncService, private validationService: ValidationService, - private configService: ConfigService, private providerApiService: ProviderApiServiceAbstraction, private formBuilder: FormBuilder, private toastService: ToastService, @@ -117,15 +110,9 @@ export class SetupComponent implements OnInit, OnDestroy { submit = async () => { try { - const requireProviderPaymentMethodDuringSetup = await firstValueFrom( - this.requireProviderPaymentMethodDuringSetup$, - ); - this.formGroup.markAllAsTouched(); - const paymentValid = requireProviderPaymentMethodDuringSetup - ? this.paymentComponent.validate() - : true; + const paymentValid = this.paymentComponent.validate(); const taxInformationValid = this.taxInformationComponent.validate(); if (!paymentValid || !taxInformationValid || !this.formGroup.valid) { @@ -152,9 +139,7 @@ export class SetupComponent implements OnInit, OnDestroy { request.taxInfo.city = taxInformation.city; request.taxInfo.state = taxInformation.state; - if (requireProviderPaymentMethodDuringSetup) { - request.paymentSource = await this.paymentComponent.tokenize(); - } + request.paymentSource = await this.paymentComponent.tokenize(); const provider = await this.providerApiService.postProviderSetup(this.providerId, request); diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index 0be7e904af3..ef8241b534c 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -181,7 +181,7 @@ bitSuffix type="button" [appCopyClick]="callbackPath" - [appA11yTitle]="'copyValue' | i18n" + [label]="'copyValue' | i18n" > @@ -193,7 +193,7 @@ bitSuffix type="button" [appCopyClick]="signedOutCallbackPath" - [appA11yTitle]="'copyValue' | i18n" + [label]="'copyValue' | i18n" > @@ -336,7 +336,7 @@ bitSuffix type="button" [appCopyClick]="spEntityId" - [appA11yTitle]="'copyValue' | i18n" + [label]="'copyValue' | i18n" > @@ -348,7 +348,7 @@ bitSuffix type="button" [appCopyClick]="spEntityIdStatic" - [appA11yTitle]="'copyValue' | i18n" + [label]="'copyValue' | i18n" > @@ -360,14 +360,14 @@ bitSuffix type="button" [appLaunchClick]="spMetadataUrl" - [appA11yTitle]="'launch' | i18n" + [label]="'launch' | i18n" > @@ -379,7 +379,7 @@ bitSuffix type="button" [appCopyClick]="spAcsUrl" - [appA11yTitle]="'copyValue' | i18n" + [label]="'copyValue' | i18n" > 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 043ce65b961..2ab82bd837b 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 @@ -86,7 +86,7 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - appA11yTitle="{{ 'options' | i18n }}" + label="{{ 'options' | i18n }}" > diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.html index ab7c67c7b22..e7f9692beb3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.html @@ -7,6 +7,7 @@ (click)="toggle()" [attr.aria-expanded]="open" [attr.aria-controls]="contentId" + [label]="'toggleVisibility' | i18n" >
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 53fc9572a21..fce1fc16d6c 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 @@ -1,13 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; +import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { ProjectListView } from "../models/view/project-list.view"; @@ -29,8 +32,22 @@ export class ProjectService { private keyService: KeyService, private apiService: ApiService, private encryptService: EncryptService, + private accountService: AccountService, ) {} + private getOrganizationKey$(organizationId: string) { + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + filter((orgKeys) => !!orgKeys), + map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), + ); + } + + private async getOrganizationKey(organizationId: string): Promise { + return await firstValueFrom(this.getOrganizationKey$(organizationId)); + } + async getByProjectId(projectId: string): Promise { const r = await this.apiService.send("GET", "/projects/" + projectId, null, true, true); const projectResponse = new ProjectResponse(r); @@ -83,10 +100,6 @@ export class ProjectService { }); } - private async getOrganizationKey(organizationId: string): Promise { - return await this.keyService.getOrgKey(organizationId); - } - private async getProjectRequest( organizationId: string, projectView: ProjectView, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts index ea5294624af..81a568f0c65 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, firstValueFrom, @@ -17,9 +17,12 @@ import { } 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 { DialogService } from "@bitwarden/components"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; import { ProjectListView } from "../../models/view/project-list.view"; +import { ProjectView } from "../../models/view/project.view"; import { BulkConfirmationDetails, BulkConfirmationDialogComponent, @@ -55,6 +58,9 @@ export class ProjectsComponent implements OnInit { private dialogService: DialogService, private organizationService: OrganizationService, private accountService: AccountService, + private toastService: ToastService, + private i18nService: I18nService, + private router: Router, ) {} ngOnInit() { @@ -73,9 +79,53 @@ export class ProjectsComponent implements OnInit { ) )?.enabled; - return await this.getProjects(); + const projects = await this.getProjects(); + const viewEvents = this.route.snapshot.queryParams.viewEvents; + + if (viewEvents) { + const targetProject = projects.find((project) => project.id === viewEvents); + + const userIsAdmin = ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ) + )?.isAdmin; + + // They would fall into here if they don't have access to a project, or if it has been permanently deleted. + if (!targetProject) { + //If they are an admin it was permanently deleted and we can show the events with project name redacted + if (userIsAdmin) { + this.openEventsDialogFromEntityId( + this.i18nService.t("nameUnavailableProjectDeleted", viewEvents), + params.organizationId, + viewEvents, + ); + } else { + //They aren't an admin so we don't know if they have access to it, lets show the unknown cipher toast. + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("unknownProject"), + }); + } + } else { + this.openEventsDialog(targetProject); + } + + await this.router.navigate([], { + queryParams: { search: this.search }, + }); + } + + return projects; }), ); + + if (this.route.snapshot.queryParams.search) { + this.search = this.route.snapshot.queryParams.search; + } } private async getProjects(): Promise { @@ -103,6 +153,30 @@ export class ProjectsComponent implements OnInit { }); } + openEventsDialog = (project: ProjectView): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: project.name, + organizationId: project.organizationId, + entityId: project.id, + entity: "project", + }, + }); + + openEventsDialogFromEntityId = ( + headerName: string, + organizationId: string, + entityId: string, + ): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: headerName, + organizationId: organizationId, + entityId: entityId, + entity: "project", + }, + }); + async openDeleteProjectDialog(projects: ProjectListView[]) { let projectsToDelete = projects; const readOnlyProjects = projects.filter((project) => project.write == false); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.html index 24168d0b025..3a2c858ac31 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.html @@ -95,7 +95,7 @@ buttonType="danger" bitIconButton="bwi-trash" bitFormButton - [appA11yTitle]="'delete' | i18n" + [label]="'delete' | i18n" [bitAction]="delete" > diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.spec.ts index 6cfc40d3676..056f7cfe255 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.spec.ts @@ -1,8 +1,14 @@ -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { SecretAccessPoliciesView } from "../models/view/access-policies/secret-access-policies.view"; @@ -11,6 +17,16 @@ import { AccessPolicyService } from "../shared/access-policies/access-policy.ser import { SecretService } from "./secret.service"; +const SomeCsprngArray = new Uint8Array(64) as CsprngArray; +const SomeOrganization = "some organization" as OrganizationId; +const AnotherOrganization = "another organization" as OrganizationId; +const SomeOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey; +const AnotherOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey; +const OrgRecords: Record = { + [SomeOrganization]: SomeOrgKey, + [AnotherOrganization]: AnotherOrgKey, +}; + describe("SecretService", () => { let sut: SecretService; @@ -18,11 +34,30 @@ describe("SecretService", () => { const apiService = mock(); const encryptService = mock(); const accessPolicyService = mock(); + let accountService: MockProxy = mock(); + const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ + id: "testId" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); beforeEach(() => { jest.resetAllMocks(); - sut = new SecretService(keyService, apiService, encryptService, accessPolicyService); + const orgKey$ = new BehaviorSubject(OrgRecords); + keyService.orgKeys$.mockReturnValue(orgKey$); + + accountService = mock(); + accountService.activeAccount$ = activeAccountSubject; + + sut = new SecretService( + keyService, + apiService, + encryptService, + accessPolicyService, + accountService, + ); encryptService.encryptString.mockResolvedValue({ encryptedString: "mockEncryptedString", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.ts index f4549dd0aee..c37a7bee2a4 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.ts @@ -1,12 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; +import { Subject, firstValueFrom, switchMap, map, filter } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { SecretAccessPoliciesView } from "../models/view/access-policies/secret-access-policies.view"; @@ -27,7 +30,6 @@ import { SecretResponse } from "./responses/secret.response"; }) export class SecretService { protected _secret: Subject = new Subject(); - secret$ = this._secret.asObservable(); constructor( @@ -35,8 +37,22 @@ export class SecretService { private apiService: ApiService, private encryptService: EncryptService, private accessPolicyService: AccessPolicyService, + private accountService: AccountService, ) {} + private getOrganizationKey$(organizationId: string) { + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + filter((orgKeys) => !!orgKeys), + map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), + ); + } + + private async getOrganizationKey(organizationId: string): Promise { + return await firstValueFrom(this.getOrganizationKey$(organizationId)); + } + async getBySecretId(secretId: string): Promise { const r = await this.apiService.send("GET", "/secrets/" + secretId, null, true, true); const secretResponse = new SecretResponse(r); @@ -154,10 +170,6 @@ export class SecretService { this._secret.next(null); } - private async getOrganizationKey(organizationId: string): Promise { - return await this.keyService.getOrgKey(organizationId); - } - private async getSecretRequest( organizationId: string, secretView: SecretView, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index 18ecd2e3b51..ca093f449c9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { combineLatestWith, firstValueFrom, Observable, startWith, switchMap } from "rxjs"; import { @@ -13,7 +13,8 @@ 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 { DialogService } from "@bitwarden/components"; +import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; import { SecretListView } from "../models/view/secret-list.view"; import { SecretsListComponent } from "../shared/secrets-list.component"; @@ -54,6 +55,8 @@ export class SecretsComponent implements OnInit { private organizationService: OrganizationService, private accountService: AccountService, private logService: LogService, + private toastService: ToastService, + private router: Router, ) {} ngOnInit() { @@ -71,7 +74,53 @@ export class SecretsComponent implements OnInit { ) )?.enabled; - return await this.getSecrets(); + const secrets = await this.getSecrets(); + const viewEvents = this.route.snapshot.queryParams.viewEvents; + + if (viewEvents) { + let targetSecret = secrets.find((secret) => secret.id === viewEvents); + + const userIsAdmin = ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ) + )?.isAdmin; + + // Secret might be deleted, make sure they are an admin before checking the trashed secrets + if (!targetSecret && userIsAdmin) { + targetSecret = (await this.secretService.getTrashedSecrets(this.organizationId)).find( + (e) => e.id == viewEvents, + ); + } + + // They would fall into here if they don't have access to a secret, or if it has been permanently deleted. + if (!targetSecret) { + //If they are an admin it was permanently deleted and we can show the events even though we don't have the secret name + if (userIsAdmin) { + this.openEventsDialogByEntityId( + this.i18nService.t("nameUnavailableSecretDeleted", viewEvents), + viewEvents, + ); + } else { + //They aren't an admin so we don't know if they have access to it, lets show the unknown secret toast. + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("unknownSecret"), + }); + } + } else { + this.openEventsDialog(targetSecret); + } + + await this.router.navigate([], { + queryParams: { search: this.search }, + }); + } + + return secrets; }), ); @@ -80,6 +129,26 @@ export class SecretsComponent implements OnInit { } } + openEventsDialogByEntityId = (secretName: string, secretId: string): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: secretName, + organizationId: this.organizationId, + entityId: secretId, + entity: "secret", + }, + }); + + openEventsDialog = (secret: SecretListView): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: secret.name, + organizationId: this.organizationId, + entityId: secret.id, + entity: "secret", + }, + }); + private async getSecrets(): Promise { return await this.secretService.getSecrets(this.organizationId); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.html index fbb0dd8888a..3399b550ba5 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.html @@ -40,8 +40,7 @@ bitIconButton="bwi-ellipsis-v" buttonType="main" [bitMenuTriggerFor]="tableMenu" - [title]="'options' | i18n" - [attr.aria-label]="'options' | i18n" + [label]="'options' | i18n" > @@ -65,8 +64,7 @@ type="button" bitIconButton="bwi-ellipsis-v" buttonType="main" - [title]="'options' | i18n" - [attr.aria-label]="'options' | i18n" + [label]="'options' | i18n" [bitMenuTriggerFor]="tokenMenu" > diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts index 9cdc176bd7b..137eadb30b5 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts @@ -1,15 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; +import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { AccessTokenRequest } from "../models/requests/access-token.request"; @@ -32,6 +35,7 @@ export class AccessService { private apiService: ApiService, private keyGenerationService: KeyGenerationService, private encryptService: EncryptService, + private accountService: AccountService, ) {} async getAccessTokens( @@ -50,6 +54,19 @@ export class AccessService { return await this.createAccessTokenViews(organizationId, results.data); } + private getOrganizationKey$(organizationId: string) { + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + filter((orgKeys) => !!orgKeys), + map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), + ); + } + + private async getOrganizationKey(organizationId: string): Promise { + return await firstValueFrom(this.getOrganizationKey$(organizationId)); + } + async createAccessToken( organizationId: string, serviceAccountId: string, @@ -117,10 +134,6 @@ export class AccessService { return accessTokenRequest; } - private async getOrganizationKey(organizationId: string): Promise { - return await this.keyService.getOrgKey(organizationId); - } - private async createAccessTokenViews( organizationId: string, accessTokenResponses: AccessTokenResponse[], diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.html index b17e47a39ec..11f8e0b9b77 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.html @@ -11,12 +11,19 @@ type="button" bitIconButton="bwi-clone" [bitAction]="copyIdentityUrl" + [label]="'copyCustomField' | i18n: identityUrl" > {{ "apiUrl" | i18n }} - +
@@ -27,6 +34,7 @@ type="button" bitIconButton="bwi-clone" [bitAction]="copyOrganizationId" + [label]="'copyCustomField' | i18n: organizationId" >
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index ddaa0937e6f..2e364df1423 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -2,8 +2,10 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { takeUntil } from "rxjs"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; @@ -25,7 +27,6 @@ export class ServiceAccountEventsComponent implements OnInit, OnDestroy { exportFileName = "machine-account-events"; - private destroy$ = new Subject(); private serviceAccountId: string; constructor( @@ -38,6 +39,8 @@ export class ServiceAccountEventsComponent logService: LogService, fileDownloadService: FileDownloadService, toastService: ToastService, + protected organizationService: OrganizationService, + protected accountService: AccountService, ) { super( eventService, @@ -47,10 +50,14 @@ export class ServiceAccountEventsComponent logService, fileDownloadService, toastService, + route, + accountService, + organizationService, ); } async ngOnInit() { + this.initBase(); // eslint-disable-next-line rxjs/no-async-subscribe this.route.params.pipe(takeUntil(this.destroy$)).subscribe(async (params) => { this.serviceAccountId = params.serviceAccountId; @@ -78,9 +85,4 @@ export class ServiceAccountEventsComponent email: "", }; } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts index 15d66ce61c3..2706bf99f3b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts @@ -1,13 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; +import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { @@ -34,8 +37,22 @@ export class ServiceAccountService { private keyService: KeyService, private apiService: ApiService, private encryptService: EncryptService, + private accountService: AccountService, ) {} + private getOrganizationKey$(organizationId: string) { + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + filter((orgKeys) => !!orgKeys), + map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), + ); + } + + private async getOrganizationKey(organizationId: string): Promise { + return await firstValueFrom(this.getOrganizationKey$(organizationId)); + } + async getServiceAccounts( organizationId: string, includeAccessToSecrets?: boolean, @@ -129,10 +146,6 @@ export class ServiceAccountService { }); } - private async getOrganizationKey(organizationId: string): Promise { - return await this.keyService.getOrgKey(organizationId); - } - private async getServiceAccountRequest( organizationKey: SymmetricCryptoKey, serviceAccountView: ServiceAccountView, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html index bfb7b985423..3d7fc9715c3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html @@ -39,8 +39,7 @@ type="button" bitIconButton="bwi-ellipsis-v" buttonType="main" - [title]="'options' | i18n" - [attr.aria-label]="'options' | i18n" + [label]="'options' | i18n" [bitMenuTriggerFor]="tableMenu" > @@ -72,8 +71,7 @@ type="button" bitIconButton="bwi-ellipsis-v" buttonType="main" - [title]="'options' | i18n" - [attr.aria-label]="'options' | i18n" + [label]="'options' | i18n" [bitMenuTriggerFor]="serviceAccountMenu" > diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts index 47d5dd63806..ac3defaf5dd 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts @@ -5,7 +5,6 @@ import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core import { Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TableDataSource, ToastService } from "@bitwarden/components"; import { @@ -49,7 +48,6 @@ export class ServiceAccountsListComponent implements OnDestroy { constructor( private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private toastService: ToastService, ) { this.selection.changed diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.spec.ts index 2e266dfbb15..a4f77e6de0b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.spec.ts @@ -1,11 +1,14 @@ -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; @@ -16,17 +19,43 @@ import { SecretsManagerImportedSecretRequest } from "../models/requests/sm-impor import { SecretsManagerPortingApiService } from "./sm-porting-api.service"; +const SomeCsprngArray = new Uint8Array(64) as CsprngArray; +const SomeOrganization = "some organization" as OrganizationId; +const AnotherOrganization = "another organization" as OrganizationId; +const SomeOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey; +const AnotherOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey; +const OrgRecords: Record = { + [SomeOrganization]: SomeOrgKey, + [AnotherOrganization]: AnotherOrgKey, +}; + describe("SecretsManagerPortingApiService", () => { let sut: SecretsManagerPortingApiService; const apiService = mock(); const encryptService = mock(); const keyService = mock(); + let accountService: MockProxy; + const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ + id: "testId" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); beforeEach(() => { jest.resetAllMocks(); - sut = new SecretsManagerPortingApiService(apiService, encryptService, keyService); + const orgKey$ = new BehaviorSubject(OrgRecords); + keyService.orgKeys$.mockReturnValue(orgKey$); + accountService = mock(); + accountService.activeAccount$ = activeAccountSubject; + sut = new SecretsManagerPortingApiService( + apiService, + encryptService, + keyService, + accountService, + ); encryptService.encryptString.mockResolvedValue(mockEncryptedString); encryptService.decryptString.mockResolvedValue(mockUnencryptedString); @@ -51,7 +80,6 @@ describe("SecretsManagerPortingApiService", () => { it("emits the import successful", async () => { const expectedRequest = toRequest([project1, project2], [secret1, secret2]); - let subscriptionCount = 0; sut.imports$.subscribe((request) => { expect(request).toBeDefined(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts index 5fb2a006144..c45f230bf7a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts @@ -1,12 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; +import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { SecretsManagerImportError } from "../models/error/sm-import-error"; @@ -31,8 +35,22 @@ export class SecretsManagerPortingApiService { private apiService: ApiService, private encryptService: EncryptService, private keyService: KeyService, + private accountService: AccountService, ) {} + private getOrganizationKey$(organizationId: string) { + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + filter((orgKeys) => !!orgKeys), + map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), + ); + } + + private async getOrganizationKey(organizationId: string): Promise { + return await firstValueFrom(this.getOrganizationKey$(organizationId)); + } + async export(organizationId: string): Promise { const response = await this.apiService.send( "GET", @@ -78,7 +96,7 @@ export class SecretsManagerPortingApiService { const encryptedImport = new SecretsManagerImportRequest(); try { - const orgKey = await this.keyService.getOrgKey(organizationId); + const orgKey = await this.getOrganizationKey(organizationId); encryptedImport.projects = []; encryptedImport.secrets = []; @@ -120,7 +138,7 @@ export class SecretsManagerPortingApiService { organizationId: string, exportData: SecretsManagerExportResponse, ): Promise { - const orgKey = await this.keyService.getOrgKey(organizationId); + const orgKey = await this.getOrganizationKey(organizationId); const decryptedExport = new SecretsManagerExport(); decryptedExport.projects = []; decryptedExport.secrets = []; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html index c8a50175781..d01faae4e6e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html @@ -67,8 +67,7 @@ buttonType="main" size="default" [disabled]="disabled" - [attr.title]="'remove' | i18n" - [attr.aria-label]="'remove' | i18n" + [label]="'remove' | i18n" (click)="selectionList.deselectItem(item.id); handleBlur()" > diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.spec.ts index 51231184d77..37a0dc06837 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.spec.ts @@ -1,12 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; @@ -30,17 +33,39 @@ import { ServiceAccountGrantedPoliciesRequest } from "./models/requests/service- import { trackEmissions } from "@bitwarden/common/../spec"; +const SomeCsprngArray = new Uint8Array(64) as CsprngArray; +const SomeOrganization = "some organization" as OrganizationId; +const AnotherOrganization = "another organization" as OrganizationId; +const SomeOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey; +const AnotherOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey; +const OrgRecords: Record = { + [SomeOrganization]: SomeOrgKey, + [AnotherOrganization]: AnotherOrgKey, +}; + describe("AccessPolicyService", () => { let sut: AccessPolicyService; const keyService = mock(); const apiService = mock(); const encryptService = mock(); + let accountService: MockProxy; + const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ + id: "testId" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); beforeEach(() => { jest.resetAllMocks(); - sut = new AccessPolicyService(keyService, apiService, encryptService); + const orgKey$ = new BehaviorSubject(OrgRecords); + keyService.orgKeys$.mockReturnValue(orgKey$); + + accountService = mock(); + accountService.activeAccount$ = activeAccountSubject; + sut = new AccessPolicyService(keyService, apiService, encryptService, accountService); }); it("instantiates", () => { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts index 87542530ecc..18f6313b95c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts @@ -1,13 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; +import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { @@ -62,8 +65,22 @@ export class AccessPolicyService { private keyService: KeyService, protected apiService: ApiService, protected encryptService: EncryptService, + private accountService: AccountService, ) {} + private getOrganizationKey$(organizationId: string) { + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + filter((orgKeys) => !!orgKeys), + map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), + ); + } + + private async getOrganizationKey(organizationId: string): Promise { + return await firstValueFrom(this.getOrganizationKey$(organizationId)); + } + async getProjectPeopleAccessPolicies( projectId: string, ): Promise { @@ -268,10 +285,6 @@ export class AccessPolicyService { }; } - private async getOrganizationKey(organizationId: string): Promise { - return await this.keyService.getOrgKey(organizationId); - } - private getAccessPolicyRequest( granteeId: string, view: UserAccessPolicyView | GroupAccessPolicyView | ServiceAccountAccessPolicyView, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html index bc1655d8b28..9e31ff628fb 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html @@ -43,8 +43,7 @@ bitIconButton="bwi-ellipsis-v" buttonType="main" [bitMenuTriggerFor]="tableMenu" - [title]="'options' | i18n" - [attr.aria-label]="'options' | i18n" + [label]="'options' | i18n" *ngIf="showMenus" > @@ -77,8 +76,7 @@ bitIconButton="bwi-clone" buttonType="main" size="small" - [title]="'copyUuid' | i18n" - [attr.aria-label]="'copyUuid' | i18n" + [label]="'copyUuid' | i18n" (click)="copyProjectUuidToClipboard(project.id)" >
@@ -94,8 +92,7 @@ bitIconButton="bwi-ellipsis-v" buttonType="main" [bitMenuTriggerFor]="projectMenu" - [title]="'options' | i18n" - [attr.aria-label]="'options' | i18n" + [label]="'options' | i18n" *ngIf="showMenus" > @@ -117,6 +114,15 @@ {{ "deleteProject" | i18n }} + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts index 9774172cd4b..31114bcd1c4 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts @@ -1,21 +1,31 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { SelectionModel } from "@angular/cdk/collections"; -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { map } from "rxjs"; +import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { catchError, concatMap, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs"; +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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { TableDataSource, ToastService } from "@bitwarden/components"; +import { DialogRef, DialogService, TableDataSource, ToastService } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; +import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; import { ProjectListView } from "../models/view/project-list.view"; +import { ProjectView } from "../models/view/project.view"; @Component({ selector: "sm-projects-list", templateUrl: "./projects-list.component.html", standalone: false, }) -export class ProjectsListComponent { +export class ProjectsListComponent implements OnInit { @Input() get projects(): ProjectListView[] { return this._projects; @@ -26,6 +36,9 @@ export class ProjectsListComponent { this.dataSource.data = projects; } private _projects: ProjectListView[]; + protected viewEventsAllowed$: Observable; + protected isAdmin$: Observable; + private destroy$: Subject = new Subject(); @Input() showMenus?: boolean = true; @@ -50,8 +63,41 @@ export class ProjectsListComponent { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private toastService: ToastService, + private dialogService: DialogService, + private organizationService: OrganizationService, + private activatedRoute: ActivatedRoute, + private accountService: AccountService, + private logService: LogService, ) {} + ngOnInit(): void { + this.viewEventsAllowed$ = this.activatedRoute.params.pipe( + concatMap((params) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), + map((org) => org.canAccessEventLogs), + catchError((error: unknown) => { + if (typeof error === "string") { + this.toastService.showToast({ + message: error, + variant: "error", + title: "", + }); + } else { + this.logService.error(error); + } + return of(false); + }), + takeUntil(this.destroy$), + ); + } + isAllSelected() { if (this.selection.selected?.length > 0) { const numSelected = this.selection.selected.length; @@ -87,6 +133,16 @@ export class ProjectsListComponent { } } + openEventsDialog = (project: ProjectView): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: project.name, + organizationId: project.organizationId, + entityId: project.id, + entity: "project", + }, + }); + private selectedHasWriteAccess() { const selectedProjects = this.projects.filter((project) => this.selection.isSelected(project.id), diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html index 859c7417eb8..25f2447246a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html @@ -45,8 +45,7 @@ type="button" bitIconButton="bwi-ellipsis-v" buttonType="main" - [title]="'options' | i18n" - [attr.aria-label]="'options' | i18n" + [label]="'options' | i18n" [bitMenuTriggerFor]="tableMenu" > @@ -78,8 +77,7 @@ bitIconButton="bwi-clone" buttonType="main" size="small" - [title]="'copyUuid' | i18n" - [attr.aria-label]="'copyUuid' | i18n" + [label]="'copyUuid' | i18n" (click)="copySecretUuidEvent.emit(secret.id)" >
@@ -108,8 +106,7 @@ type="button" bitIconButton="bwi-ellipsis-v" buttonType="main" - [title]="'options' | i18n" - [attr.aria-label]="'options' | i18n" + [label]="'options' | i18n" [bitMenuTriggerFor]="secretMenu" > @@ -151,6 +148,15 @@ {{ "restoreSecret" | i18n }} + - - - -
- -
-
- diff --git a/libs/angular/src/auth/components/environment-selector.component.ts b/libs/angular/src/auth/components/environment-selector.component.ts deleted file mode 100644 index 1831e513301..00000000000 --- a/libs/angular/src/auth/components/environment-selector.component.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { ConnectedPosition } from "@angular/cdk/overlay"; -import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { Observable, map, Subject, takeUntil } from "rxjs"; - -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular"; -import { - EnvironmentService, - Region, - RegionConfig, -} from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -export const ExtensionDefaultOverlayPosition: ConnectedPosition[] = [ - { - originX: "start", - originY: "top", - overlayX: "start", - overlayY: "bottom", - }, -]; -export const DesktopDefaultOverlayPosition: ConnectedPosition[] = [ - { - originX: "start", - originY: "top", - overlayX: "start", - overlayY: "bottom", - }, -]; - -export interface EnvironmentSelectorRouteData { - overlayPosition?: ConnectedPosition[]; -} - -@Component({ - selector: "environment-selector", - templateUrl: "environment-selector.component.html", - animations: [ - trigger("transformPanel", [ - state( - "void", - style({ - opacity: 0, - }), - ), - transition( - "void => open", - animate( - "100ms linear", - style({ - opacity: 1, - }), - ), - ), - transition("* => void", animate("100ms linear", style({ opacity: 0 }))), - ]), - ], - standalone: false, -}) -export class EnvironmentSelectorComponent implements OnInit, OnDestroy { - @Output() onOpenSelfHostedSettings = new EventEmitter(); - @Input() overlayPosition: ConnectedPosition[] = [ - { - originX: "start", - originY: "bottom", - overlayX: "start", - overlayY: "top", - }, - ]; - - protected isOpen = false; - protected ServerEnvironmentType = Region; - protected availableRegions = this.environmentService.availableRegions(); - protected selectedRegion$: Observable = - this.environmentService.environment$.pipe( - map((e) => e.getRegion()), - map((r) => this.availableRegions.find((ar) => ar.key === r)), - ); - - private destroy$ = new Subject(); - - constructor( - protected environmentService: EnvironmentService, - private route: ActivatedRoute, - private dialogService: DialogService, - private toastService: ToastService, - private i18nService: I18nService, - ) {} - - ngOnInit() { - this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data) => { - if (data && data["overlayPosition"]) { - this.overlayPosition = data["overlayPosition"]; - } - }); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - async toggle(option: Region) { - this.isOpen = !this.isOpen; - if (option === null) { - return; - } - - /** - * Opens the self-hosted settings dialog when the self-hosted option is selected. - */ - if (option === Region.SelfHosted) { - const dialogResult = await SelfHostedEnvConfigDialogComponent.open(this.dialogService); - if (dialogResult) { - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("environmentSaved"), - }); - } - // Don't proceed to setEnvironment when the self-hosted dialog is cancelled - return; - } - - await this.environmentService.setEnvironment(option); - } - - close() { - this.isOpen = false; - } -} diff --git a/libs/angular/src/auth/device-management/device-management-item-group.component.html b/libs/angular/src/auth/device-management/device-management-item-group.component.html index b47408059a2..b6a3ea2d8f8 100644 --- a/libs/angular/src/auth/device-management/device-management-item-group.component.html +++ b/libs/angular/src/auth/device-management/device-management-item-group.component.html @@ -6,8 +6,7 @@ bit-item-content type="button" [attr.tabindex]="device.pendingAuthRequest != null ? 0 : null" - (click)="approveOrDenyAuthRequest(device.pendingAuthRequest)" - (keydown.enter)="approveOrDenyAuthRequest(device.pendingAuthRequest)" + (click)="answerAuthRequest(device.pendingAuthRequest)" > {{ device.displayName }} @@ -21,7 +20,7 @@ - {{ "needsApproval" | i18n }} +
- @let showFooterBorder = !isDrawer || background() === "alt" || bodyHasScrolledFrom().bottom; + @let showFooterBorder = + (!bodyHasScrolledFrom().top && isScrollable) || bodyHasScrolledFrom().bottom;
diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index fc780ea853e..e10d78046dd 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -1,7 +1,15 @@ import { CdkTrapFocus } from "@angular/cdk/a11y"; import { CdkScrollable } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { Component, HostBinding, inject, viewChild, input, booleanAttribute } from "@angular/core"; +import { + Component, + HostBinding, + inject, + viewChild, + input, + booleanAttribute, + AfterViewInit, +} from "@angular/core"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -31,10 +39,11 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai CdkScrollable, ], }) -export class DialogComponent { +export class DialogComponent implements AfterViewInit { protected dialogRef = inject(DialogRef, { optional: true }); private scrollableBody = viewChild.required(CdkScrollable); protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody); + protected isScrollable = false; /** Background color */ readonly background = input<"default" | "alt">("default"); @@ -96,4 +105,13 @@ export class DialogComponent { } } } + + ngAfterViewInit() { + this.isScrollable = this.canScroll(); + } + + canScroll(): boolean { + const el = this.scrollableBody().getElementRef().nativeElement as HTMLElement; + return el.scrollHeight > el.clientHeight; + } } diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index 7cf8b774a23..f93ef1a2f25 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -64,6 +64,13 @@ export default { disable: true, }, }, + background: { + options: ["alt", "default"], + control: { type: "radio" }, + table: { + defaultValue: "default", + }, + }, }, parameters: { design: { @@ -94,8 +101,7 @@ export const Default: Story = { bitIconButton="bwi-trash" buttonType="danger" size="default" - title="Delete" - aria-label="Delete"> + label="Delete"> `, @@ -144,7 +150,7 @@ export const ScrollingContent: Story = { render: (args) => ({ props: args, template: /*html*/ ` - + Dialog body text goes here.
@@ -168,7 +174,7 @@ export const TabContent: Story = { render: (args) => ({ props: args, template: /*html*/ ` - + First Tab Content @@ -205,14 +211,14 @@ export const WithCards: Story = { }, template: /*html*/ `
- +

Foo

- +
@@ -232,7 +238,7 @@ export const WithCards: Story = {

Bar

- + @@ -258,8 +264,7 @@ export const WithCards: Story = { bitIconButton="bwi-trash" buttonType="danger" size="default" - title="Delete" - aria-label="Delete"> + label="Delete">
@@ -269,5 +274,6 @@ export const WithCards: Story = { dialogSize: "default", title: "Default", subtitle: "Subtitle", + background: "alt", }, }; 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 c1044ba81e2..4c04f0e29c9 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,5 +1,3 @@ -// 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 { Component, Inject } from "@angular/core"; import { FormGroup, ReactiveFormsModule } from "@angular/forms"; @@ -47,10 +45,10 @@ export class SimpleConfigurableDialogComponent { ]; } - protected title: string; - protected content: string; - protected acceptButtonText: string; - protected cancelButtonText: string; + protected title?: string; + protected content?: string; + protected acceptButtonText?: string; + protected cancelButtonText?: string; protected formGroup = new FormGroup({}); protected showCancelButton = this.simpleDialogOpts.cancelButtonText !== null; @@ -58,7 +56,7 @@ export class SimpleConfigurableDialogComponent { constructor( public dialogRef: DialogRef, private i18nService: I18nService, - @Inject(DIALOG_DATA) public simpleDialogOpts?: SimpleDialogOptions, + @Inject(DIALOG_DATA) public simpleDialogOpts: SimpleDialogOptions, ) { this.localizeText(); } @@ -76,24 +74,27 @@ export class SimpleConfigurableDialogComponent { private localizeText() { this.title = this.translate(this.simpleDialogOpts.title); this.content = this.translate(this.simpleDialogOpts.content); - this.acceptButtonText = this.translate(this.simpleDialogOpts.acceptButtonText, "yes"); + this.acceptButtonText = this.translate( + this.simpleDialogOpts.acceptButtonText ?? { key: "yes" }, + ); if (this.showCancelButton) { // If accept text is overridden, use cancel, otherwise no this.cancelButtonText = this.translate( - this.simpleDialogOpts.cancelButtonText, - this.simpleDialogOpts.acceptButtonText !== undefined ? "cancel" : "no", + this.simpleDialogOpts.cancelButtonText ?? + (this.simpleDialogOpts.acceptButtonText !== undefined + ? { key: "cancel" } + : { key: "no" }), ); } } - private translate(translation: string | Translation, defaultKey?: string): string { - // Translation interface use implies we must localize. + private translate(translation: string | Translation): string { + // Object implies we must localize. if (typeof translation === "object") { return this.i18nService.t(translation.key, ...(translation.placeholders ?? [])); } - // Use string that is already translated or use default key post translate - return translation ?? this.i18nService.t(defaultKey); + return translation; } } 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 47cd396a239..07f6048c12a 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html @@ -1,5 +1,5 @@
diff --git a/libs/components/src/disclosure/disclosure-trigger-for.directive.ts b/libs/components/src/disclosure/disclosure-trigger-for.directive.ts index 9524d518df7..78bc9f2b038 100644 --- a/libs/components/src/disclosure/disclosure-trigger-for.directive.ts +++ b/libs/components/src/disclosure/disclosure-trigger-for.directive.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Directive, HostBinding, HostListener, input } from "@angular/core"; import { DisclosureComponent } from "./disclosure.component"; @@ -12,7 +10,7 @@ export class DisclosureTriggerForDirective { /** * Accepts template reference for a bit-disclosure component instance */ - readonly disclosure = input(undefined, { alias: "bitDisclosureTriggerFor" }); + readonly disclosure = input.required({ alias: "bitDisclosureTriggerFor" }); @HostBinding("attr.aria-expanded") get ariaExpanded() { return this.disclosure().open; diff --git a/libs/components/src/disclosure/disclosure.component.ts b/libs/components/src/disclosure/disclosure.component.ts index 7d26815d817..2d73d7d8ad6 100644 --- a/libs/components/src/disclosure/disclosure.component.ts +++ b/libs/components/src/disclosure/disclosure.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, EventEmitter, @@ -30,6 +28,7 @@ let nextId = 0; * bitIconButton="bwi-sliders" * [buttonType]="'muted'" * [bitDisclosureTriggerFor]="disclosureRef" + * [label]="'Settings' | i18n" * > * click button to hide this content * ``` @@ -40,11 +39,10 @@ let nextId = 0; template: ``, }) export class DisclosureComponent { - private _open: boolean; - /** Emits the visibility of the disclosure content */ @Output() openChange = new EventEmitter(); + private _open?: boolean; /** * Optionally init the disclosure in its opened state */ @@ -54,14 +52,13 @@ export class DisclosureComponent { this._open = isOpen; this.openChange.emit(isOpen); } + get open(): boolean { + return !!this._open; + } @HostBinding("class") get classList() { return this.open ? "" : "tw-hidden"; } @HostBinding("id") id = `bit-disclosure-${nextId++}`; - - get open(): boolean { - return this._open; - } } diff --git a/libs/components/src/disclosure/disclosure.stories.ts b/libs/components/src/disclosure/disclosure.stories.ts index bb3680c1f3b..2e45964ccaa 100644 --- a/libs/components/src/disclosure/disclosure.stories.ts +++ b/libs/components/src/disclosure/disclosure.stories.ts @@ -27,7 +27,7 @@ export const DisclosureWithIconButton: Story = { render: (args) => ({ props: args, template: /*html*/ ` - click button to hide this content `, diff --git a/libs/components/src/drawer/drawer-header.component.html b/libs/components/src/drawer/drawer-header.component.html index 863b19edfb2..2723744eda3 100644 --- a/libs/components/src/drawer/drawer-header.component.html +++ b/libs/components/src/drawer/drawer-header.component.html @@ -5,11 +5,5 @@ {{ title() }}
{{ "firstLogin" | i18n }}: {{ device.firstLogin | date: "medium" }} diff --git a/libs/angular/src/auth/device-management/device-management-item-group.component.ts b/libs/angular/src/auth/device-management/device-management-item-group.component.ts index 864712ceb78..71e343e734f 100644 --- a/libs/angular/src/auth/device-management/device-management-item-group.component.ts +++ b/libs/angular/src/auth/device-management/device-management-item-group.component.ts @@ -1,15 +1,11 @@ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; -import { firstValueFrom } from "rxjs"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; -import { BadgeModule, DialogService, ItemModule } from "@bitwarden/components"; +import { BadgeModule, ItemModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -import { LoginApprovalDialogComponent } from "../login-approval/login-approval-dialog.component"; - import { DeviceDisplayData } from "./device-management.component"; -import { clearAuthRequestAndResortDevices } from "./resort-devices.helper"; /** Displays user devices in an item list view */ @Component({ @@ -20,24 +16,12 @@ import { clearAuthRequestAndResortDevices } from "./resort-devices.helper"; }) export class DeviceManagementItemGroupComponent { @Input() devices: DeviceDisplayData[] = []; + @Output() onAuthRequestAnswered = new EventEmitter(); - constructor(private dialogService: DialogService) {} - - protected async approveOrDenyAuthRequest(pendingAuthRequest: DevicePendingAuthRequest | null) { + protected answerAuthRequest(pendingAuthRequest: DevicePendingAuthRequest | null) { if (pendingAuthRequest == null) { return; } - - const loginApprovalDialog = LoginApprovalDialogComponent.open(this.dialogService, { - notificationId: pendingAuthRequest.id, - }); - - const result = await firstValueFrom(loginApprovalDialog.closed); - - if (result !== undefined && typeof result === "boolean") { - // Auth request was approved or denied, so clear the - // pending auth request and re-sort the device array - this.devices = clearAuthRequestAndResortDevices(this.devices, pendingAuthRequest); - } + this.onAuthRequestAnswered.emit(pendingAuthRequest); } } diff --git a/libs/angular/src/auth/device-management/device-management-table.component.html b/libs/angular/src/auth/device-management/device-management-table.component.html index febb0a96a4e..72187b2a2fc 100644 --- a/libs/angular/src/auth/device-management/device-management-table.component.html +++ b/libs/angular/src/auth/device-management/device-management-table.component.html @@ -1,4 +1,4 @@ - + @@ -17,24 +16,17 @@ - +
@if (device.pendingAuthRequest) { - + {{ device.displayName }} -
- {{ "needsApproval" | i18n }} -
+
} @else { {{ device.displayName }}
diff --git a/libs/angular/src/auth/device-management/device-management-table.component.ts b/libs/angular/src/auth/device-management/device-management-table.component.ts index c3c835f05ed..d663e28b9e4 100644 --- a/libs/angular/src/auth/device-management/device-management-table.component.ts +++ b/libs/angular/src/auth/device-management/device-management-table.component.ts @@ -1,6 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, Input, OnChanges, SimpleChanges } from "@angular/core"; -import { firstValueFrom } from "rxjs"; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; @@ -8,16 +7,12 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { BadgeModule, ButtonModule, - DialogService, LinkModule, TableDataSource, TableModule, } from "@bitwarden/components"; -import { LoginApprovalDialogComponent } from "../login-approval/login-approval-dialog.component"; - import { DeviceDisplayData } from "./device-management.component"; -import { clearAuthRequestAndResortDevices } from "./resort-devices.helper"; /** Displays user devices in a sortable table view */ @Component({ @@ -28,6 +23,8 @@ import { clearAuthRequestAndResortDevices } from "./resort-devices.helper"; }) export class DeviceManagementTableComponent implements OnChanges { @Input() devices: DeviceDisplayData[] = []; + @Output() onAuthRequestAnswered = new EventEmitter(); + protected tableDataSource = new TableDataSource(); protected readonly columnConfig = [ @@ -51,10 +48,7 @@ export class DeviceManagementTableComponent implements OnChanges { }, ]; - constructor( - private i18nService: I18nService, - private dialogService: DialogService, - ) {} + constructor(private i18nService: I18nService) {} ngOnChanges(changes: SimpleChanges): void { if (changes.devices) { @@ -62,24 +56,10 @@ export class DeviceManagementTableComponent implements OnChanges { } } - protected async approveOrDenyAuthRequest(pendingAuthRequest: DevicePendingAuthRequest | null) { + protected answerAuthRequest(pendingAuthRequest: DevicePendingAuthRequest | null) { if (pendingAuthRequest == null) { return; } - - const loginApprovalDialog = LoginApprovalDialogComponent.open(this.dialogService, { - notificationId: pendingAuthRequest.id, - }); - - const result = await firstValueFrom(loginApprovalDialog.closed); - - if (result !== undefined && typeof result === "boolean") { - // Auth request was approved or denied, so clear the - // pending auth request and re-sort the device array - this.tableDataSource.data = clearAuthRequestAndResortDevices( - this.devices, - pendingAuthRequest, - ); - } + this.onAuthRequestAnswered.emit(pendingAuthRequest); } } diff --git a/libs/angular/src/auth/device-management/device-management.component.html b/libs/angular/src/auth/device-management/device-management.component.html index 6ee50a32e8e..2a91c2daae2 100644 --- a/libs/angular/src/auth/device-management/device-management.component.html +++ b/libs/angular/src/auth/device-management/device-management.component.html @@ -30,11 +30,13 @@ } diff --git a/libs/angular/src/auth/device-management/device-management.component.ts b/libs/angular/src/auth/device-management/device-management.component.ts index dc7700a9410..3ab9b2146c5 100644 --- a/libs/angular/src/auth/device-management/device-management.component.ts +++ b/libs/angular/src/auth/device-management/device-management.component.ts @@ -16,14 +16,18 @@ import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; -import { ButtonModule, PopoverModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, PopoverModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +import { LoginApprovalDialogComponent } from "../login-approval"; + import { DeviceManagementComponentServiceAbstraction } from "./device-management-component.service.abstraction"; import { DeviceManagementItemGroupComponent } from "./device-management-item-group.component"; import { DeviceManagementTableComponent } from "./device-management-table.component"; +import { clearAuthRequestAndResortDevices, resortDevices } from "./resort-devices.helper"; export interface DeviceDisplayData { + creationDate: string; displayName: string; firstLogin: Date; icon: string; @@ -66,6 +70,7 @@ export class DeviceManagementComponent implements OnInit { private destroyRef: DestroyRef, private deviceManagementComponentService: DeviceManagementComponentServiceAbstraction, private devicesService: DevicesServiceAbstraction, + private dialogService: DialogService, private i18nService: I18nService, private messageListener: MessageListener, private validationService: ValidationService, @@ -130,6 +135,7 @@ export class DeviceManagementComponent implements OnInit { } return { + creationDate: device.creationDate, displayName: this.devicesService.getReadableDeviceTypeName(device.type), firstLogin: device.creationDate ? new Date(device.creationDate) : new Date(), icon: this.getDeviceIcon(device.type), @@ -141,7 +147,8 @@ export class DeviceManagementComponent implements OnInit { pendingAuthRequest: device.response?.devicePendingAuthRequest ?? null, }; }) - .filter((device) => device !== null); + .filter((device) => device !== null) + .sort(resortDevices); } private async upsertDeviceWithPendingAuthRequest(authRequestId: string) { @@ -151,6 +158,7 @@ export class DeviceManagementComponent implements OnInit { } const upsertDevice: DeviceDisplayData = { + creationDate: "", displayName: this.devicesService.getReadableDeviceTypeName( authRequestResponse.requestDeviceTypeValue, ), @@ -174,8 +182,9 @@ export class DeviceManagementComponent implements OnInit { ); if (existingDevice?.id && existingDevice.creationDate) { - upsertDevice.id = existingDevice.id; + upsertDevice.creationDate = existingDevice.creationDate; upsertDevice.firstLogin = new Date(existingDevice.creationDate); + upsertDevice.id = existingDevice.id; } } @@ -186,10 +195,10 @@ export class DeviceManagementComponent implements OnInit { if (existingDeviceIndex >= 0) { // Update existing device in device list this.devices[existingDeviceIndex] = upsertDevice; - this.devices = [...this.devices]; + this.devices = [...this.devices].sort(resortDevices); } else { // Add new device to device list - this.devices = [upsertDevice, ...this.devices]; + this.devices = [upsertDevice, ...this.devices].sort(resortDevices); } } @@ -227,4 +236,18 @@ export class DeviceManagementComponent implements OnInit { const metadata = DeviceTypeMetadata[type]; return metadata ? (categoryIconMap[metadata.category] ?? defaultIcon) : defaultIcon; } + + protected async handleAuthRequestAnswered(pendingAuthRequest: DevicePendingAuthRequest) { + const loginApprovalDialog = LoginApprovalDialogComponent.open(this.dialogService, { + notificationId: pendingAuthRequest.id, + }); + + const result = await firstValueFrom(loginApprovalDialog.closed); + + if (result !== undefined && typeof result === "boolean") { + // Auth request was approved or denied, so clear the + // pending auth request and re-sort the device array + this.devices = clearAuthRequestAndResortDevices(this.devices, pendingAuthRequest); + } + } } diff --git a/libs/angular/src/auth/device-management/resort-devices.helper.ts b/libs/angular/src/auth/device-management/resort-devices.helper.ts index e739e943ee8..01661c18ef3 100644 --- a/libs/angular/src/auth/device-management/resort-devices.helper.ts +++ b/libs/angular/src/auth/device-management/resort-devices.helper.ts @@ -23,7 +23,7 @@ export function clearAuthRequestAndResortDevices( * * This is a helper function that gets passed to the `Array.sort()` method */ -function resortDevices(deviceA: DeviceDisplayData, deviceB: DeviceDisplayData) { +export function resortDevices(deviceA: DeviceDisplayData, deviceB: DeviceDisplayData) { // Devices with a pending auth request should be first if (deviceA.pendingAuthRequest) { return -1; @@ -40,11 +40,11 @@ function resortDevices(deviceA: DeviceDisplayData, deviceB: DeviceDisplayData) { return 1; } - // Then sort the rest by display name (alphabetically) - if (deviceA.displayName < deviceB.displayName) { + // Then sort the rest by creation date (newest to oldest) + if (deviceA.creationDate > deviceB.creationDate) { return -1; } - if (deviceA.displayName > deviceB.displayName) { + if (deviceA.creationDate < deviceB.creationDate) { return 1; } diff --git a/libs/angular/src/auth/environment-selector/environment-selector.component.html b/libs/angular/src/auth/environment-selector/environment-selector.component.html new file mode 100644 index 00000000000..f6484ea1e5f --- /dev/null +++ b/libs/angular/src/auth/environment-selector/environment-selector.component.html @@ -0,0 +1,48 @@ + +
+ + + + +
+ {{ "accessing" | i18n }}: + +
+
+
diff --git a/libs/angular/src/auth/environment-selector/environment-selector.component.ts b/libs/angular/src/auth/environment-selector/environment-selector.component.ts new file mode 100644 index 00000000000..6fe3eaa92a0 --- /dev/null +++ b/libs/angular/src/auth/environment-selector/environment-selector.component.ts @@ -0,0 +1,75 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy } from "@angular/core"; +import { Observable, map, Subject } from "rxjs"; + +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular"; +import { + EnvironmentService, + Region, + RegionConfig, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + DialogService, + LinkModule, + MenuModule, + ToastService, + TypographyModule, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +@Component({ + selector: "environment-selector", + templateUrl: "environment-selector.component.html", + standalone: true, + imports: [CommonModule, I18nPipe, MenuModule, LinkModule, TypographyModule], +}) +export class EnvironmentSelectorComponent implements OnDestroy { + protected ServerEnvironmentType = Region; + protected availableRegions = this.environmentService.availableRegions(); + protected selectedRegion$: Observable = + this.environmentService.environment$.pipe( + map((e) => e.getRegion()), + map((r) => this.availableRegions.find((ar) => ar.key === r)), + ); + + private destroy$ = new Subject(); + + constructor( + public environmentService: EnvironmentService, + private dialogService: DialogService, + private toastService: ToastService, + private i18nService: I18nService, + ) {} + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + async toggle(option: Region) { + if (option === null) { + return; + } + + /** + * Opens the self-hosted settings dialog when the self-hosted option is selected. + */ + if (option === Region.SelfHosted) { + const dialogResult = await SelfHostedEnvConfigDialogComponent.open(this.dialogService); + if (dialogResult) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("environmentSaved"), + }); + } + // Don't proceed to setEnvironment when the self-hosted dialog is cancelled + return; + } + + await this.environmentService.setEnvironment(option); + } +} diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts new file mode 100644 index 00000000000..c662e20b275 --- /dev/null +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts @@ -0,0 +1,262 @@ +import { CommonModule } from "@angular/common"; +import { SimpleChange } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ReactiveFormsModule } from "@angular/forms"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SelectModule, FormFieldModule, BitSubmitDirective } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { ManageTaxInformationComponent } from "./manage-tax-information.component"; + +describe("ManageTaxInformationComponent", () => { + let sut: ManageTaxInformationComponent; + let fixture: ComponentFixture; + let mockTaxService: MockProxy; + + beforeEach(async () => { + mockTaxService = mock(); + await TestBed.configureTestingModule({ + declarations: [ManageTaxInformationComponent], + providers: [ + { provide: TaxServiceAbstraction, useValue: mockTaxService }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + imports: [ + CommonModule, + ReactiveFormsModule, + SelectModule, + FormFieldModule, + BitSubmitDirective, + I18nPipe, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ManageTaxInformationComponent); + sut = fixture.componentInstance; + fixture.autoDetectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("creates successfully", () => { + expect(sut).toBeTruthy(); + }); + + it("should initialize with all values empty in startWith", async () => { + // Arrange + sut.startWith = { + country: "", + postalCode: "", + taxId: "", + line1: "", + line2: "", + city: "", + state: "", + }; + + // Act + fixture.detectChanges(); + + // Assert + const startWithValue = sut.startWith; + expect(startWithValue.line1).toHaveLength(0); + expect(startWithValue.line2).toHaveLength(0); + expect(startWithValue.city).toHaveLength(0); + expect(startWithValue.state).toHaveLength(0); + expect(startWithValue.postalCode).toHaveLength(0); + expect(startWithValue.country).toHaveLength(0); + expect(startWithValue.taxId).toHaveLength(0); + }); + + it("should update the tax information protected state when form is updated", async () => { + // Arrange + const line1Value = "123 Street"; + const line2Value = "Apt. 5"; + const cityValue = "New York"; + const stateValue = "NY"; + const countryValue = "USA"; + const postalCodeValue = "123 Street"; + + sut.startWith = { + country: countryValue, + postalCode: "", + taxId: "", + line1: "", + line2: "", + city: "", + state: "", + }; + sut.showTaxIdField = false; + mockTaxService.isCountrySupported.mockResolvedValue(true); + + // Act + await sut.ngOnInit(); + fixture.detectChanges(); + + const line1: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='line1']", + ); + const line2: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='line2']", + ); + const city: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='city']", + ); + const state: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='state']", + ); + const postalCode: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='postalCode']", + ); + + line1.value = line1Value; + line2.value = line2Value; + city.value = cityValue; + state.value = stateValue; + postalCode.value = postalCodeValue; + + line1.dispatchEvent(new Event("input")); + line2.dispatchEvent(new Event("input")); + city.dispatchEvent(new Event("input")); + state.dispatchEvent(new Event("input")); + postalCode.dispatchEvent(new Event("input")); + await fixture.whenStable(); + + // Assert + + // Assert that the internal tax information reflects the form + const taxInformation = sut.getTaxInformation(); + expect(taxInformation.line1).toBe(line1Value); + expect(taxInformation.line2).toBe(line2Value); + expect(taxInformation.city).toBe(cityValue); + expect(taxInformation.state).toBe(stateValue); + expect(taxInformation.postalCode).toBe(postalCodeValue); + expect(taxInformation.country).toBe(countryValue); + expect(taxInformation.taxId).toHaveLength(0); + + expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); + expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(2); + }); + + it("should not show address fields except postal code if country is not supported for taxes", async () => { + // Arrange + const countryValue = "UNKNOWN"; + sut.startWith = { + country: countryValue, + postalCode: "", + taxId: "", + line1: "", + line2: "", + city: "", + state: "", + }; + sut.showTaxIdField = false; + mockTaxService.isCountrySupported.mockResolvedValue(false); + + // Act + await sut.ngOnInit(); + fixture.detectChanges(); + + const line1: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='line1']", + ); + const line2: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='line2']", + ); + const city: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='city']", + ); + const state: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='state']", + ); + const postalCode: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='postalCode']", + ); + + // Assert + expect(line1).toBeNull(); + expect(line2).toBeNull(); + expect(city).toBeNull(); + expect(state).toBeNull(); + //Should be visible + expect(postalCode).toBeTruthy(); + + expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); + expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); + }); + + it("should not show the tax id field if showTaxIdField is set to false", async () => { + // Arrange + const countryValue = "USA"; + sut.startWith = { + country: countryValue, + postalCode: "", + taxId: "", + line1: "", + line2: "", + city: "", + state: "", + }; + + sut.showTaxIdField = false; + mockTaxService.isCountrySupported.mockResolvedValue(true); + + // Act + await sut.ngOnInit(); + fixture.detectChanges(); + + // Assert + const taxId: HTMLInputElement = fixture.nativeElement.querySelector( + "input[formControlName='taxId']", + ); + expect(taxId).toBeNull(); + + expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); + expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); + }); + + it("should clear the tax id field if showTaxIdField is set to false after being true", async () => { + // Arrange + const countryValue = "USA"; + const taxIdValue = "A12345678"; + + sut.startWith = { + country: countryValue, + postalCode: "", + taxId: taxIdValue, + line1: "", + line2: "", + city: "", + state: "", + }; + sut.showTaxIdField = true; + + mockTaxService.isCountrySupported.mockResolvedValue(true); + await sut.ngOnInit(); + fixture.detectChanges(); + const initialTaxIdValue = fixture.nativeElement.querySelector( + "input[formControlName='taxId']", + ).value; + + // Act + sut.showTaxIdField = false; + sut.ngOnChanges({ showTaxIdField: new SimpleChange(true, false, false) }); + fixture.detectChanges(); + + // Assert + const taxId = fixture.nativeElement.querySelector("input[formControlName='taxId']"); + expect(taxId).toBeNull(); + + const taxInformation = sut.getTaxInformation(); + expect(taxInformation.taxId).toBeNull(); + expect(initialTaxIdValue).toEqual(taxIdValue); + + expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); + expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); + }); +}); 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 57306d66b4b..0b87f3f931d 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 @@ -1,6 +1,15 @@ // 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 { + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, +} from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; import { debounceTime } from "rxjs/operators"; @@ -13,7 +22,7 @@ import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/model templateUrl: "./manage-tax-information.component.html", standalone: false, }) -export class ManageTaxInformationComponent implements OnInit, OnDestroy { +export class ManageTaxInformationComponent implements OnInit, OnDestroy, OnChanges { @Input() startWith: TaxInformation; @Input() onSubmit?: (taxInformation: TaxInformation) => Promise; @Input() showTaxIdField: boolean = true; @@ -56,7 +65,7 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { } submit = async () => { - this.formGroup.markAllAsTouched(); + this.markAllAsTouched(); if (this.formGroup.invalid) { return; } @@ -65,7 +74,7 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { }; validate(): boolean { - this.formGroup.markAllAsTouched(); + this.markAllAsTouched(); return this.formGroup.valid; } @@ -142,6 +151,14 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { }); } + ngOnChanges(changes: SimpleChanges): void { + // Clear the value of the tax-id if states have been changed in the parent component + const showTaxIdField = changes["showTaxIdField"]; + if (showTaxIdField && !showTaxIdField.currentValue) { + this.formGroup.controls.taxId.setValue(null); + } + } + ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 2122506890a..6bf3ab77252 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -13,7 +13,6 @@ import { ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { Theme } from "@bitwarden/common/platform/enums"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message } from "@bitwarden/common/platform/messaging"; import { HttpOperations } from "@bitwarden/common/services/api.service"; import { SafeInjectionToken } from "@bitwarden/ui-common"; @@ -33,7 +32,6 @@ export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken< >("OBSERVABLE_DISK_LOCAL_STORAGE"); export const MEMORY_STORAGE = new SafeInjectionToken("MEMORY_STORAGE"); export const SECURE_STORAGE = new SafeInjectionToken("SECURE_STORAGE"); -export const STATE_FACTORY = new SafeInjectionToken("STATE_FACTORY"); export const LOGOUT_CALLBACK = new SafeInjectionToken< (logoutReason: LogoutReason, userId?: string) => Promise >("LOGOUT_CALLBACK"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 6111e64d30e..24dbb273b26 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -151,6 +151,7 @@ import { OrganizationBillingApiService } from "@bitwarden/common/billing/service import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { TaxService } from "@bitwarden/common/billing/services/tax.service"; +import { DefaultKeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; @@ -186,7 +187,6 @@ import { } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -197,13 +197,10 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service"; import { ActionsService } from "@bitwarden/common/platform/actions"; import { UnsupportedActionsService } from "@bitwarden/common/platform/actions/unsupported-actions.service"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; 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 { DefaultTaskSchedulerService, TaskSchedulerService, @@ -230,13 +227,13 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; 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"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { ActiveUserAccessor, ActiveUserStateProvider, + DefaultStateService, DerivedStateProvider, GlobalStateProvider, SingleUserStateProvider, @@ -254,7 +251,7 @@ import { StateEventRunnerService } from "@bitwarden/common/platform/state/state- import { SyncService } from "@bitwarden/common/platform/sync"; // eslint-disable-next-line no-restricted-imports -- Needed for DI import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal"; -import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications/system-notifications.service"; +import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications"; import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service"; import { DefaultThemeStateService, @@ -375,12 +372,10 @@ import { LOCKED_CALLBACK, LOG_MAC_FAILURES, LOGOUT_CALLBACK, - MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, SECURE_STORAGE, - STATE_FACTORY, SUPPORTS_SECURE_STORAGE, SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, @@ -418,10 +413,6 @@ const safeProviders: SafeProvider[] = [ useFactory: (window: Window) => window.navigator.language, deps: [WINDOW], }), - safeProvider({ - provide: STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }), // TODO: PM-21212 - Deprecate LogoutCallback in favor of LogoutService safeProvider({ provide: LOGOUT_CALLBACK, @@ -524,7 +515,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DomainSettingsService, useClass: DefaultDomainSettingsService, - deps: [StateProvider, ConfigService], + deps: [StateProvider], }), safeProvider({ provide: CipherServiceAbstraction, @@ -534,7 +525,6 @@ const safeProviders: SafeProvider[] = [ apiService: ApiServiceAbstraction, i18nService: I18nServiceAbstraction, searchService: SearchServiceAbstraction, - stateService: StateServiceAbstraction, autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, @@ -551,7 +541,6 @@ const safeProviders: SafeProvider[] = [ apiService, i18nService, searchService, - stateService, autofillSettingsService, encryptService, fileUploadService, @@ -568,7 +557,6 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, I18nServiceAbstraction, SearchServiceAbstraction, - StateServiceAbstraction, AutofillSettingsServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, @@ -666,15 +654,15 @@ const safeProviders: SafeProvider[] = [ GlobalStateProvider, SUPPORTS_SECURE_STORAGE, SECURE_STORAGE, - KeyGenerationServiceAbstraction, + KeyGenerationService, EncryptService, LogService, LOGOUT_CALLBACK, ], }), safeProvider({ - provide: KeyGenerationServiceAbstraction, - useClass: KeyGenerationService, + provide: KeyGenerationService, + useClass: DefaultKeyGenerationService, deps: [CryptoFunctionServiceAbstraction], }), safeProvider({ @@ -683,7 +671,7 @@ const safeProviders: SafeProvider[] = [ deps: [ PinServiceAbstraction, InternalMasterPasswordServiceAbstraction, - KeyGenerationServiceAbstraction, + KeyGenerationService, CryptoFunctionServiceAbstraction, EncryptService, PlatformUtilsServiceAbstraction, @@ -773,7 +761,7 @@ const safeProviders: SafeProvider[] = [ deps: [ KeyService, I18nServiceAbstraction, - KeyGenerationServiceAbstraction, + KeyGenerationService, SendStateProviderAbstraction, EncryptService, ], @@ -805,7 +793,6 @@ const safeProviders: SafeProvider[] = [ InternalSendService, LogService, KeyConnectorServiceAbstraction, - StateServiceAbstraction, ProviderServiceAbstraction, FolderApiServiceAbstraction, InternalOrganizationServiceAbstraction, @@ -853,6 +840,7 @@ const safeProviders: SafeProvider[] = [ MessagingServiceAbstraction, SearchServiceAbstraction, StateServiceAbstraction, + TokenServiceAbstraction, AuthServiceAbstraction, VaultTimeoutSettingsService, StateEventRunnerService, @@ -872,24 +860,10 @@ const safeProviders: SafeProvider[] = [ useClass: SsoLoginService, deps: [StateProvider, LogService], }), - safeProvider({ - provide: STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }), safeProvider({ provide: StateServiceAbstraction, - useClass: StateService, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - STATE_FACTORY, - AccountServiceAbstraction, - EnvironmentService, - TokenServiceAbstraction, - MigrationRunner, - ], + useClass: DefaultStateService, + deps: [AbstractStorageService, SECURE_STORAGE, ActiveUserAccessor], }), safeProvider({ provide: IndividualVaultExportServiceAbstraction, @@ -1049,7 +1023,7 @@ const safeProviders: SafeProvider[] = [ deps: [ StateProvider, StateServiceAbstraction, - KeyGenerationServiceAbstraction, + KeyGenerationService, EncryptService, LogService, CryptoFunctionServiceAbstraction, @@ -1071,7 +1045,7 @@ const safeProviders: SafeProvider[] = [ TokenServiceAbstraction, LogService, OrganizationServiceAbstraction, - KeyGenerationServiceAbstraction, + KeyGenerationService, LOGOUT_CALLBACK, StateProvider, ], @@ -1230,7 +1204,7 @@ const safeProviders: SafeProvider[] = [ provide: DeviceTrustServiceAbstraction, useClass: DeviceTrustService, deps: [ - KeyGenerationServiceAbstraction, + KeyGenerationService, CryptoFunctionServiceAbstraction, KeyService, EncryptService, @@ -1266,7 +1240,7 @@ const safeProviders: SafeProvider[] = [ CryptoFunctionServiceAbstraction, EncryptService, KdfConfigService, - KeyGenerationServiceAbstraction, + KeyGenerationService, LogService, StateProvider, ], diff --git a/libs/angular/src/vault/components/spotlight/spotlight.component.html b/libs/angular/src/vault/components/spotlight/spotlight.component.html index e445640cff9..0d0e95e191b 100644 --- a/libs/angular/src/vault/components/spotlight/spotlight.component.html +++ b/libs/angular/src/vault/components/spotlight/spotlight.component.html @@ -18,9 +18,8 @@ size="small" *ngIf="!persistent" (click)="handleDismiss()" - [attr.title]="'close' | i18n" - [attr.aria-label]="'close' | i18n" class="-tw-me-2" + [label]="'close' | i18n" >
diff --git a/libs/angular/src/vault/services/nudges.service.spec.ts b/libs/angular/src/vault/services/nudges.service.spec.ts index 7c4c8ba8b74..cba973bd894 100644 --- a/libs/angular/src/vault/services/nudges.service.spec.ts +++ b/libs/angular/src/vault/services/nudges.service.spec.ts @@ -8,7 +8,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; @@ -31,10 +30,6 @@ describe("Vault Nudges Service", () => { let fakeStateProvider: FakeStateProvider; let testBed: TestBed; - const mockConfigService = { - getFeatureFlag$: jest.fn().mockReturnValue(of(true)), - getFeatureFlag: jest.fn().mockReturnValue(true), - }; const nudgeServices = [ EmptyVaultNudgeService, @@ -58,7 +53,6 @@ describe("Vault Nudges Service", () => { provide: StateProvider, useValue: fakeStateProvider, }, - { provide: ConfigService, useValue: mockConfigService }, { provide: HasItemsNudgeService, useValue: mock(), diff --git a/libs/angular/src/vault/services/nudges.service.ts b/libs/angular/src/vault/services/nudges.service.ts index 6cb7ae4abf1..4c77ff38bf6 100644 --- a/libs/angular/src/vault/services/nudges.service.ts +++ b/libs/angular/src/vault/services/nudges.service.ts @@ -1,8 +1,6 @@ import { inject, Injectable } from "@angular/core"; -import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs"; +import { combineLatest, map, Observable, shareReplay } from "rxjs"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { UserKeyDefinition, NUDGES_DISK } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; @@ -83,7 +81,6 @@ export class NudgesService { * @private */ private defaultNudgeService = inject(DefaultSingleNudgeService); - private configService = inject(ConfigService); private getNudgeService(nudge: NudgeType): SingleNudgeService { return this.customNudgeServices[nudge] ?? this.defaultNudgeService; @@ -95,16 +92,9 @@ export class NudgesService { * @param userId */ showNudgeSpotlight$(nudge: NudgeType, userId: UserId): Observable { - return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe( - switchMap((hasVaultNudgeFlag) => { - if (!hasVaultNudgeFlag) { - return of(false); - } - return this.getNudgeService(nudge) - .nudgeStatus$(nudge, userId) - .pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed)); - }), - ); + return this.getNudgeService(nudge) + .nudgeStatus$(nudge, userId) + .pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed)); } /** @@ -113,16 +103,9 @@ export class NudgesService { * @param userId */ showNudgeBadge$(nudge: NudgeType, userId: UserId): Observable { - return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe( - switchMap((hasVaultNudgeFlag) => { - if (!hasVaultNudgeFlag) { - return of(false); - } - return this.getNudgeService(nudge) - .nudgeStatus$(nudge, userId) - .pipe(map((nudgeStatus) => !nudgeStatus.hasBadgeDismissed)); - }), - ); + return this.getNudgeService(nudge) + .nudgeStatus$(nudge, userId) + .pipe(map((nudgeStatus) => !nudgeStatus.hasBadgeDismissed)); } /** @@ -131,14 +114,7 @@ export class NudgesService { * @param userId */ showNudgeStatus$(nudge: NudgeType, userId: UserId) { - return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe( - switchMap((hasVaultNudgeFlag) => { - if (!hasVaultNudgeFlag) { - return of({ hasBadgeDismissed: true, hasSpotlightDismissed: true } as NudgeStatus); - } - return this.getNudgeService(nudge).nudgeStatus$(nudge, userId); - }), - ); + return this.getNudgeService(nudge).nudgeStatus$(nudge, userId); } /** diff --git a/libs/auth/src/angular/input-password/input-password.component.html b/libs/auth/src/angular/input-password/input-password.component.html index bf3a51b98bb..d56fe6a27fc 100644 --- a/libs/auth/src/angular/input-password/input-password.component.html +++ b/libs/auth/src/angular/input-password/input-password.component.html @@ -45,7 +45,7 @@ type="button" bitIconButton="bwi-generate" bitSuffix - [appA11yTitle]="'generatePassword' | i18n" + [label]="'generatePassword' | i18n" (click)="generatePassword()" > - + ``` ## `[bitSubmit]` Disabled Form Submit diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index dd901cd2477..88383fe85a3 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -7,6 +7,7 @@ import { delay, of } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { A11yTitleDirective } from "../a11y"; import { ButtonModule } from "../button"; import { FormFieldModule } from "../form-field"; import { IconButtonModule } from "../icon-button"; @@ -28,20 +29,21 @@ const template = ` Email - + - + `; @Component({ selector: "app-promise-example", template, imports: [ + A11yTitleDirective, AsyncActionsModule, ButtonModule, FormFieldModule, @@ -86,6 +88,7 @@ class PromiseExampleComponent { selector: "app-observable-example", template, imports: [ + A11yTitleDirective, AsyncActionsModule, ButtonModule, FormFieldModule, diff --git a/libs/components/src/async-actions/standalone.mdx b/libs/components/src/async-actions/standalone.mdx index f484ea01c58..a781f40d852 100644 --- a/libs/components/src/async-actions/standalone.mdx +++ b/libs/components/src/async-actions/standalone.mdx @@ -63,7 +63,7 @@ from how click handlers are usually defined with the output syntax `(click)="han ```html -`; +`; ``` ## Stories diff --git a/libs/components/src/async-actions/standalone.stories.ts b/libs/components/src/async-actions/standalone.stories.ts index 1ed6f6c5a59..99cde70566b 100644 --- a/libs/components/src/async-actions/standalone.stories.ts +++ b/libs/components/src/async-actions/standalone.stories.ts @@ -16,7 +16,7 @@ const template = /*html*/ ` - `; + `; @Component({ template, diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 7c41d4e59a0..6f83c9ca101 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { NgClass } from "@angular/common"; import { Component, OnChanges, input } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; @@ -9,11 +7,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall"; const SizeClasses: Record = { - xlarge: ["tw-h-24", "tw-w-24"], - large: ["tw-h-16", "tw-w-16"], - default: ["tw-h-10", "tw-w-10"], - small: ["tw-h-7", "tw-w-7"], - xsmall: ["tw-h-6", "tw-w-6"], + xlarge: ["tw-h-24", "tw-w-24", "tw-min-w-24"], + large: ["tw-h-16", "tw-w-16", "tw-min-w-16"], + default: ["tw-h-10", "tw-w-10", "tw-min-w-10"], + small: ["tw-h-7", "tw-w-7", "tw-min-w-7"], + xsmall: ["tw-h-6", "tw-w-6", "tw-min-w-6"], }; /** @@ -41,7 +39,7 @@ export class AvatarComponent implements OnChanges { private svgFontSize = 20; private svgFontWeight = 300; private svgSize = 48; - src: SafeResourceUrl; + src?: SafeResourceUrl; constructor(public sanitizer: DomSanitizer) {} @@ -50,14 +48,20 @@ export class AvatarComponent implements OnChanges { } get classList() { - return ["tw-rounded-full"] + return ["tw-rounded-full", "tw-inline"] .concat(SizeClasses[this.size()] ?? []) .concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []); } private generate() { - let chars: string = null; - const upperCaseText = this.text()?.toUpperCase() ?? ""; + const color = this.color(); + const text = this.text(); + const id = this.id(); + if (!text && !color && !id) { + throw new Error("Must supply `text`, `color`, or `id` input."); + } + let chars: string | null = null; + const upperCaseText = text?.toUpperCase() ?? ""; chars = this.getFirstLetters(upperCaseText, this.svgCharCount); @@ -66,18 +70,17 @@ export class AvatarComponent implements OnChanges { } // If the chars contain an emoji, only show it. - if (chars.match(Utils.regexpEmojiPresentation)) { - chars = chars.match(Utils.regexpEmojiPresentation)[0]; + const emojiMatch = chars.match(Utils.regexpEmojiPresentation); + if (emojiMatch) { + chars = emojiMatch[0]; } let svg: HTMLElement; - let hexColor = this.color(); - - const id = this.id(); - if (!Utils.isNullOrWhitespace(this.color())) { + let hexColor = color ?? ""; + if (!Utils.isNullOrWhitespace(hexColor)) { svg = this.createSvgElement(this.svgSize, hexColor); - } else if (!Utils.isNullOrWhitespace(id)) { - hexColor = Utils.stringToColor(id.toString()); + } else if (!Utils.isNullOrWhitespace(id ?? "")) { + hexColor = Utils.stringToColor(id!.toString()); svg = this.createSvgElement(this.svgSize, hexColor); } else { hexColor = Utils.stringToColor(upperCaseText); @@ -95,7 +98,7 @@ export class AvatarComponent implements OnChanges { ); } - private getFirstLetters(data: string, count: number): string { + private getFirstLetters(data: string, count: number): string | null { const parts = data.split(" "); if (parts.length > 1) { let text = ""; diff --git a/libs/components/src/banner/banner.component.html b/libs/components/src/banner/banner.component.html index 581a56d86cb..bfde8135da9 100644 --- a/libs/components/src/banner/banner.component.html +++ b/libs/components/src/banner/banner.component.html @@ -19,8 +19,7 @@ buttonType="main" size="small" (click)="onClose.emit()" - [attr.title]="'close' | i18n" - [attr.aria-label]="'close' | i18n" + [label]="'close' | i18n" > }
diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.ts b/libs/components/src/breadcrumbs/breadcrumb.component.ts index 54678f3e4ee..783cb2655f7 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumb.component.ts @@ -1,7 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore - -import { Component, EventEmitter, Output, TemplateRef, ViewChild, input } from "@angular/core"; +import { Component, EventEmitter, Output, TemplateRef, input, viewChild } from "@angular/core"; import { QueryParamsHandling } from "@angular/router"; @Component({ @@ -20,7 +17,7 @@ export class BreadcrumbComponent { @Output() click = new EventEmitter(); - @ViewChild(TemplateRef, { static: true }) content: TemplateRef; + readonly content = viewChild(TemplateRef); onClick(args: unknown) { this.click.next(args); diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.html b/libs/components/src/breadcrumbs/breadcrumbs.component.html index 820b100afd3..b63b21de76b 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.html +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.html @@ -8,7 +8,7 @@ [queryParams]="breadcrumb.queryParams()" [queryParamsHandling]="breadcrumb.queryParamsHandling()" > - + } @else { } @if (!last) { @@ -35,6 +35,7 @@ bitIconButton="bwi-ellipsis-h" [bitMenuTriggerFor]="overflowMenu" size="small" + [label]="'moreBreadcrumbs' | i18n" > @for (breadcrumb of overflow; track breadcrumb) { @@ -46,11 +47,11 @@ [queryParams]="breadcrumb.queryParams()" [queryParamsHandling]="breadcrumb.queryParamsHandling()" > - + } @else { } } @@ -66,7 +67,7 @@ [queryParams]="breadcrumb.queryParams()" [queryParamsHandling]="breadcrumb.queryParamsHandling()" > - + } @else { } @if (!last) { diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.ts b/libs/components/src/breadcrumbs/breadcrumbs.component.ts index a1a6e732459..3c24f91be99 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.ts @@ -2,6 +2,8 @@ import { CommonModule } from "@angular/common"; import { Component, ContentChildren, QueryList, input } from "@angular/core"; import { RouterModule } from "@angular/router"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { IconButtonModule } from "../icon-button"; import { LinkModule } from "../link"; import { MenuModule } from "../menu"; @@ -16,7 +18,7 @@ import { BreadcrumbComponent } from "./breadcrumb.component"; @Component({ selector: "bit-breadcrumbs", templateUrl: "./breadcrumbs.component.html", - imports: [CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule], + imports: [I18nPipe, CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule], }) export class BreadcrumbsComponent { readonly show = input(3); diff --git a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts index 98af3c0ae7b..893f645a913 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts @@ -2,9 +2,12 @@ import { Component, importProvidersFrom } from "@angular/core"; import { RouterModule } from "@angular/router"; import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + import { IconButtonModule } from "../icon-button"; import { LinkModule } from "../link"; import { MenuModule } from "../menu"; +import { I18nMockService } from "../utils"; import { BreadcrumbComponent } from "./breadcrumb.component"; import { BreadcrumbsComponent } from "./breadcrumbs.component"; @@ -26,6 +29,16 @@ export default { decorators: [ moduleMetadata({ imports: [LinkModule, MenuModule, IconButtonModule, RouterModule, BreadcrumbComponent], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + moreBreadcrumbs: "More breadcrumbs", + }); + }, + }, + ], }), applicationConfig({ providers: [ diff --git a/libs/components/src/callout/callout.component.html b/libs/components/src/callout/callout.component.html index b990e57a767..b98679766d5 100644 --- a/libs/components/src/callout/callout.component.html +++ b/libs/components/src/callout/callout.component.html @@ -1,6 +1,6 @@

- + diff --git a/libs/components/src/drawer/drawer.stories.ts b/libs/components/src/drawer/drawer.stories.ts index a524c9a7a1a..1d7aa79137d 100644 --- a/libs/components/src/drawer/drawer.stories.ts +++ b/libs/components/src/drawer/drawer.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { RouterTestingModule } from "@angular/router/testing"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; diff --git a/libs/components/src/form-control/form-control.abstraction.ts b/libs/components/src/form-control/form-control.abstraction.ts index 4c0a543e672..ac5a249667d 100644 --- a/libs/components/src/form-control/form-control.abstraction.ts +++ b/libs/components/src/form-control/form-control.abstraction.ts @@ -1,8 +1,6 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore export abstract class BitFormControlAbstraction { - disabled: boolean; - required: boolean; - hasError: boolean; - error: [string, any]; + abstract disabled: boolean; + abstract required: boolean; + abstract hasError: boolean; + abstract error: [string, any]; } diff --git a/libs/components/src/form-control/form-control.component.html b/libs/components/src/form-control/form-control.component.html index 15d422b01a1..3dc3f90c9b0 100644 --- a/libs/components/src/form-control/form-control.component.html +++ b/libs/components/src/form-control/form-control.component.html @@ -1,11 +1,11 @@